on master help sign in
Redesign L1 transactions, OS, ID, and invites
1a8e5f0Will Galebach
src/assets/keyfile.svgnew file+5
@@ -0,0 +1,5 @@
1+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
2+<path fill-rule="evenodd" clip-rule="evenodd" d="M4.05556 5.11111C4.91467 5.11111 5.61111 4.41467 5.61111 3.55556C5.61111 2.69645 4.91467 2 4.05556 2C3.19645 2 2.5 2.69645 2.5 3.55556C2.5 4.41467 3.19645 5.11111 4.05556 5.11111ZM4.05556 6.11111C5.46695 6.11111 6.61111 4.96695 6.61111 3.55556C6.61111 2.14416 5.46695 1 4.05556 1C2.64416 1 1.5 2.14416 1.5 3.55556C1.5 4.96695 2.64416 6.11111 4.05556 6.11111Z" fill="white"/>
3+<path fill-rule="evenodd" clip-rule="evenodd" d="M3.55554 5.47217H4.55554V8.8055H5.99988V9.8055H4.55554V10.7222H6.6111V11.7222H4.55554V12.4999H3.55554V5.47217Z" fill="white"/>
4+<path fill-rule="evenodd" clip-rule="evenodd" d="M9.5 2.5H7V1.5H10.2071L13.5 4.79289V14.5H3.5V13.5H12.5V5.5H9.5V2.5ZM10.5 3.2071L11.7929 4.5H10.5V3.2071Z" fill="white"/>
5+</svg>
src/components/InlineEthereumTransaction.js+1−1
@@ -1,6 +1,6 @@
1import React, { useMemo, useCallback, useState } from 'react';
1import React, { useMemo, useCallback, useState } from 'react';
2import cn from 'classnames';
2import cn from 'classnames';
3−import { Grid, ErrorText, Flex, LinkButton, H5, Text } from 'indigo-react';
3+import { Grid, ErrorText, Flex, LinkButton, H5 } from 'indigo-react';
4import { toBN } from 'web3-utils';
4import { toBN } from 'web3-utils';
5
5
6import { useExploreTxUrls } from 'lib/explorer';
6import { useExploreTxUrls } from 'lib/explorer';
src/components/L2/Card.tsx+4−2
@@ -8,7 +8,8 @@ export interface CardProps {
8 title?: string;
8 title?: string;
9 subtitle?: string;
9 subtitle?: string;
10 onClick?: () => void;
10 onClick?: () => void;
11+ disabled?: boolean;
11− children: React.ReactNode;
12+ children?: React.ReactNode;
12}
13}
13
14
14export default function Card({
15export default function Card({
@@ -17,9 +18,10 @@ export default function Card({
17 subtitle,
18 subtitle,
18 onClick,
19 onClick,
19 children,
20 children,
21+ disabled = false,
20}: CardProps) {
22}: CardProps) {
21 return (
23 return (
22− <Box className="card" onClick={onClick}>
24+ <Box className="card" onClick={disabled ? undefined : onClick}>
23 {(!!icon || !!title) && (
25 {(!!icon || !!title) && (
24 <Row>
26 <Row>
25 {!!icon && icon}
27 {!!icon && icon}
src/components/L2/Dropdowns/AccountsDropdown.tsx+2−1
@@ -81,7 +81,8 @@ const AccountsDropdown = ({ showMigrate = false }: AccountsDropdownProps) => {
81 toggleOpen={() => setOpen(!open)}>
81 toggleOpen={() => setOpen(!open)}>
82 <CopiableAddressWrap
82 <CopiableAddressWrap
83 className="current-address"
83 className="current-address"
84− position={TooltipPosition.Left}>
84+ position={TooltipPosition.Left}
85+ text={currentAddress}>
85 {displayAddress}
86 {displayAddress}
86 </CopiableAddressWrap>
87 </CopiableAddressWrap>
87 <Box className="divider" />
88 <Box className="divider" />
src/components/L2/Headers/L2BackHeader.tsx+5−2
@@ -59,16 +59,19 @@ const L2BackHeader = ({ back, hideBalance = false }: L2BackHeaderProps) => {
59 }
59 }
60 }, [currentL2, setEthBalance, wallet, web3]);
60 }, [currentL2, setEthBalance, wallet, web3]);
61
61
62+ const hasBalance = Number(ethBalance.toString()) !== 0;
63+
62 return (
64 return (
63 <Row className="l2-back-header">
65 <Row className="l2-back-header">
64 <Icon className="back-button" icon="ChevronWest" onClick={goBack} />
66 <Icon className="back-button" icon="ChevronWest" onClick={goBack} />
65− {currentL2 ? (
67+ {currentL2 && !hideBalance ? (
66 <Row className="rollup-timer">
68 <Row className="rollup-timer">
67 <Icon icon="Clock" />
69 <Icon icon="Clock" />
68 {nextRoll}
70 {nextRoll}
69 </Row>
71 </Row>
70 ) : (
72 ) : (
71− !hideBalance && (
73+ !hideBalance &&
74+ hasBalance && (
72 <Row className="eth-balance">
75 <Row className="eth-balance">
73 <Wallet className="wallet-icon" />
76 <Wallet className="wallet-icon" />
74 <Box>{ethBalance.toString()} ETH</Box>
77 <Box>{ethBalance.toString()} ETH</Box>
src/components/L2/Headers/L2PointHeader.tsx+3−3
@@ -15,9 +15,9 @@ import { isStar } from 'lib/utils/point';
15
15
16export interface L2PointHeaderProps {
16export interface L2PointHeaderProps {
17 numInvites: number;
17 numInvites: number;
18− hideTimer: boolean;
18+ hideTimer?: boolean;
19− hideInvites: boolean;
19+ hideInvites?: boolean;
20− showMigrate: boolean;
20+ showMigrate?: boolean;
21}
21}
22
22
23const L2PointHeader = ({
23const L2PointHeader = ({
src/components/L2/Window/BodyPane.scss+56−1
@@ -10,4 +10,59 @@
10 &.spaced {
10 &.spaced {
11 justify-content: space-between;
11 justify-content: space-between;
12 }
12 }
13+
14+ .between-row {
15+ display: flex;
16+ flex-direction: row;
17+ justify-content: space-between;
18+ align-items: center;
19+ margin: 16px 0;
20+ width: 100%;
21+ }
22+
23+ .subtitle {
24+ margin-top: 4px;
25+ color: $mid-gray;
26+ }
27+
28+ .arrow-button {
29+ height: 16px;
30+ width: 16px;
31+ padding: 0 4px;
32+ }
33+
34+ button.secondary {
35+ background-color: $washed-gray;
36+ border-radius: $medium-border-radius;
37+ border: none;
38+ height: 32px;
39+ padding: 8px 16px;
40+ font-family: $default-font;
41+
42+ &:nth-child(2) {
43+ margin-left: 8px;
44+ }
45+
46+ &:hover {
47+ opacity: 0.8;
48+ }
49+ }
50+
51+ .copy-button {
52+ background-color: $washed-gray;
53+ border-radius: $medium-border-radius;
54+ width: 32px;
55+ height: 32px;
56+ display: flex;
57+ flex-direction: column;
58+ justify-content: center;
59+ align-items: center;
60+ margin-left: 8px;
61+
62+ svg {
63+ font-size: 16px;
64+ margin-top: 6px;
65+ margin-right: 4px;
66+ }
67+ }
13−}
68+}
src/components/L2/Window/HeaderPane.scss+22
@@ -11,4 +11,26 @@
11 h5 {
11 h5 {
12 font-weight: 600;
12 font-weight: 600;
13 }
13 }
14+
15+ .header-row {
16+ width: 100%;
17+ display: flex;
18+ flex-direction: row;
19+ justify-content: space-between;
20+ align-items: center;
21+
22+ .header-button {
23+ background-color: black;
24+ color: white;
25+ border-radius: $medium-border-radius;
26+ border: none;
27+ height: 32px;
28+ padding: 8px 16px;
29+ font-family: $default-font;
30+
31+ &:hover {
32+ opacity: 0.8;
33+ }
34+ }
35+ }
14}
36}
src/components/L2/Window/Window.tsx+3−2
@@ -4,10 +4,11 @@ import './Window.scss';
4
4
5export interface WindowProps {
5export interface WindowProps {
6 children: React.ReactNode;
6 children: React.ReactNode;
7+ className?: string;
7}
8}
8
9
9−const Window = ({ children }: WindowProps) => {
10+const Window = ({ children, className }: WindowProps) => {
10− return <Box className="window">{children}</Box>;
11+ return <Box className={`window ${className}`}>{children}</Box>;
11};
12};
12
13
13export default Window;
14export default Window;
src/components/Passport.js+2−7
@@ -6,13 +6,8 @@ import 'style/anim.css';
6import { sigil, reactRenderer } from 'urbit-sigil-js';
6import { sigil, reactRenderer } from 'urbit-sigil-js';
7import { Icon } from '@tlon/indigo-react';
7import { Icon } from '@tlon/indigo-react';
8
8
9−import * as need from 'lib/need';
10import { chunkStr, Matrix, walk, rand } from 'lib/card';
9import { chunkStr, Matrix, walk, rand } from 'lib/card';
11−import usePermissionsForPoint from 'lib/usePermissionsForPoint';
12import { useSyncDetails } from 'lib/useSyncPoints';
10import { useSyncDetails } from 'lib/useSyncPoints';
13−import { buildKeyType } from 'lib/point';
14−
15−import { useWallet } from 'store/wallet';
16
11
17import './SigilMini.scss';
12import './SigilMini.scss';
18
13
@@ -40,9 +35,9 @@ const symbols = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13];
40function Passport({
35function Passport({
41 address,
36 address,
42 point,
37 point,
43− inverted,
38+ inverted = false,
44 animationMode = 'none',
39 animationMode = 'none',
45− keyType,
40+ keyType = '',
46}) {
41}) {
47 const [cols, rows, tile] = [35, 12, 12];
42 const [cols, rows, tile] = [35, 12, 12];
48
43
src/components/copiable/CopiableWithTooltip.js+3−3
@@ -1,15 +1,15 @@
1import React from 'react';
1import React from 'react';
2
2
3import useCopiable from 'lib/useCopiable';
3import useCopiable from 'lib/useCopiable';
4−import WithTooltip from 'components/WithTooltip';
4+import WithTooltip, { TooltipPosition } from 'components/WithTooltip';
5import { Icon } from '@tlon/indigo-react';
5import { Icon } from '@tlon/indigo-react';
6
6
7export default function CopiableWithTooltip({
7export default function CopiableWithTooltip({
8 as: As = 'span',
8 as: As = 'span',
9 text,
9 text,
10− children,
10+ children = null,
11 className,
11 className,
12− position,
12+ position = TooltipPosition.Top,
13 ...rest
13 ...rest
14}) {
14}) {
15 const [doCopy, didCopy] = useCopiable(text || children);
15 const [doCopy, didCopy] = useCopiable(text || children);
src/indigo-react/components/Grid.d.ts+2−1
@@ -2,6 +2,7 @@ import React from 'react';
2
2
3const Grid = {
3const Grid = {
4 Item: React.ReactNode,
4 Item: React.ReactNode,
5+ Divider: React.ReactNode,
5−}
6+};
6
7
7export default Grid;
8export default Grid;
src/lib/constants.ts+7
@@ -124,6 +124,10 @@ const POINT_PROXIES = {
124const DEFAULT_FADE_TIMEOUT = 300;
124const DEFAULT_FADE_TIMEOUT = 300;
125const MASTER_KEY_DURATION = 850;
125const MASTER_KEY_DURATION = 850;
126
126
127+const INVITES_PER_PAGE = 7;
128+const DEFAULT_NUM_INVITES = 5;
129+const DEFAULT_CSV_NAME = 'urbit_invites.csv';
130+
127export {
131export {
128 CHECK_BLOCK_EVERY_MS,
132 CHECK_BLOCK_EVERY_MS,
129 DEFAULT_GAS_PRICE_GWEI,
133 DEFAULT_GAS_PRICE_GWEI,
@@ -156,4 +160,7 @@ export {
156 POINT_PROXIES,
160 POINT_PROXIES,
157 DEFAULT_FADE_TIMEOUT,
161 DEFAULT_FADE_TIMEOUT,
158 MASTER_KEY_DURATION,
162 MASTER_KEY_DURATION,
163+ INVITES_PER_PAGE,
164+ DEFAULT_NUM_INVITES,
165+ DEFAULT_CSV_NAME,
159};
166};
src/lib/types/Invite.ts−1
@@ -3,5 +3,4 @@ export interface Invite {
3 planet: number;
3 planet: number;
4 hash: string;
4 hash: string;
5 owner: string;
5 owner: string;
6− status: 'pending' | 'sending' | 'claimed' | 'available';
7}
6}
src/lib/useRoller.ts+110−93
@@ -2,24 +2,19 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
2import _ from 'lodash';
2import _ from 'lodash';
3import * as azimuth from 'azimuth-js';
3import * as azimuth from 'azimuth-js';
4import * as need from 'lib/need';
4import * as need from 'lib/need';
5−import * as ob from 'urbit-ob';
6import { Just } from 'folktale/maybe';
5import { Just } from 'folktale/maybe';
7
6
8import { Invite } from 'lib/types/Invite';
7import { Invite } from 'lib/types/Invite';
9import { convertToInt } from './convertToInt';
8import { convertToInt } from './convertToInt';
10import { isDevelopment, isRopsten } from './flags';
9import { isDevelopment, isRopsten } from './flags';
11−import { hasPoint, generateInviteWallet } from './utils/roller';
10+import { hasInvite, generateInviteWallet } from './utils/roller';
12−import { ROLLER_HOSTS } from './constants';
11+import { INVITES_PER_PAGE, ROLLER_HOSTS } from './constants';
13
12
14import { useNetwork } from 'store/network';
13import { useNetwork } from 'store/network';
15import { usePointCursor } from 'store/pointCursor';
14import { usePointCursor } from 'store/pointCursor';
16import { useRollerStore } from 'store/roller';
15import { useRollerStore } from 'store/roller';
17import { useWallet } from 'store/wallet';
16import { useWallet } from 'store/wallet';
18−import {
19− getStoredInvites,
20− setPendingInvites,
21− setStoredInvites,
22−} from 'store/storage/roller';
17+import { getStoredInvites, setStoredInvites } from 'store/storage/roller';
23import { usePointCache } from 'store/pointCache';
18import { usePointCache } from 'store/pointCache';
24import { getOutgoingPoints } from 'views/Points';
19import { getOutgoingPoints } from 'views/Points';
25import {
20import {
@@ -63,6 +58,18 @@ function isShipNumber(ship: number | string | undefined): ship is number {
63 return (ship as number) !== undefined;
58 return (ship as number) !== undefined;
64}
59}
65
60
61+const inviteTemplate = (
62+ planet: number,
63+ ticket = '',
64+ owner = '',
65+ hash = ''
66+): Invite => ({
67+ planet,
68+ ticket,
69+ owner,
70+ hash,
71+});
72+
66export default function useRoller() {
73export default function useRoller() {
67 const { wallet, authToken, authMnemonic, urbitWallet }: any = useWallet();
74 const { wallet, authToken, authMnemonic, urbitWallet }: any = useWallet();
68 const { pointCursor }: any = usePointCursor();
75 const { pointCursor }: any = usePointCursor();
@@ -73,11 +80,14 @@ export default function useRoller() {
73
80
74 const {
81 const {
75 nextBatchTime,
82 nextBatchTime,
83+ currentL2,
84+ invitePoints,
76 setNextBatchTime,
85 setNextBatchTime,
77 setNextRoll,
86 setNextRoll,
78 setPendingTransactions,
87 setPendingTransactions,
79 setInvites,
88 setInvites,
80− currentL2,
89+ setInvitePoints,
90+ setInviteGeneratingNum,
81 } = useRollerStore();
91 } = useRollerStore();
82 const [config, setConfig] = useState<Config | null>(null);
92 const [config, setConfig] = useState<Config | null>(null);
83
93
@@ -176,6 +186,7 @@ export default function useRoller() {
176 });
186 });
177
187
178 for (let i = 0; i < numInvites && planets[i]; i++) {
188 for (let i = 0; i < numInvites && planets[i]; i++) {
189+ setInviteGeneratingNum(i + 1);
179 const planet = planets[i];
190 const planet = planets[i];
180 const nonceInc = i + nonce;
191 const nonceInc = i + nonce;
181
192
@@ -237,16 +248,6 @@ export default function useRoller() {
237 owner: inviteWallet.ownership.keys.address,
248 owner: inviteWallet.ownership.keys.address,
238 });
249 });
239 }
250 }
240−
241− const hashes = await Promise.all(requests);
242−
243− const pendingInvites: Invite[] = tickets.map((ticket, ind) => ({
244− ...ticket,
245− hash: hashes[ind * 3 + 2],
246− status: 'pending',
247− }));
248−
249− setPendingInvites(_point, pendingInvites);
250 },
251 },
251 [
252 [
252 api,
253 api,
@@ -257,6 +258,7 @@ export default function useRoller() {
257 web3,
258 web3,
258 getDetails,
259 getDetails,
259 authMnemonic,
260 authMnemonic,
261+ setInviteGeneratingNum,
260 ]
262 ]
261 );
263 );
262
264
@@ -284,6 +286,7 @@ export default function useRoller() {
284 try {
286 try {
285 const curPoint = Number(need.point(pointCursor));
287 const curPoint = Number(need.point(pointCursor));
286 const newPending = await api.getPendingByShip(curPoint);
288 const newPending = await api.getPendingByShip(curPoint);
289+
287 setPendingTransactions(newPending);
290 setPendingTransactions(newPending);
288
291
289 // const allTransactions = await api.getHistory()
292 // const allTransactions = await api.getHistory()
@@ -293,102 +296,75 @@ export default function useRoller() {
293 }, [api, setPendingTransactions, pointCursor]);
296 }, [api, setPendingTransactions, pointCursor]);
294
297
295 const getInvites = useCallback(
298 const getInvites = useCallback(
296− async (isL2: boolean) => {
299+ async (isL2: boolean, page: number, getAll = false) => {
297 try {
300 try {
298 const curPoint: number = Number(need.point(pointCursor));
301 const curPoint: number = Number(need.point(pointCursor));
299− const invites = getStoredInvites(curPoint);
302+ const storedInvites = getStoredInvites(curPoint);
300− const availableInvites = invites.available;
303+ if (storedInvites.length) {
301− const pendingTransactions = await api.getPendingByShip(curPoint);
304+ setInvites(storedInvites);
302− if (isDevelopment) {
305+ } else {
303− console.log('PENDING', pendingTransactions);
306+ setInvites(invitePoints.map(p => inviteTemplate(p)));
304 }
307 }
305− setPendingTransactions(pendingTransactions);
306−
307− const stillPending = invites.pending.filter(invite => {
308− const completed = !pendingTransactions.find(
309− p => `~${p?.rawTx?.tx?.data?.ship}` === ob.patp(invite.planet)
310− );
311−
312− if (
313− completed &&
314− !availableInvites.find(({ planet }) => invite.planet === planet)
315− ) {
316− availableInvites.push({ ...invite, status: 'available' });
317− }
318−
319− return !completed;
320− });
321−
322− setStoredInvites(curPoint, {
323− available: availableInvites,
324− pending: stillPending,
325− });
326− setInvites(availableInvites);
327
308
328 const _authToken = authToken.getOrElse(null);
309 const _authToken = authToken.getOrElse(null);
329 const _contracts = contracts.getOrElse(null);
310 const _contracts = contracts.getOrElse(null);
330
311
331 if (_authToken && _contracts) {
312 if (_authToken && _contracts) {
332− let possibleMissingInvites: number[] = [];
313+ const spawnedPoints = isL2 ? await api.getSpawned(curPoint) : [];
333− let availablePoints: number[] = [];
314+
334− if (isL2) {
335− possibleMissingInvites = await api.getSpawned(curPoint);
336− availablePoints = await api.getUnspawned(curPoint);
337− } else {
338− // TODO: should it be removed?
339− availablePoints = await azimuth.azimuth.getUnspawnedChildren(
315+ const availablePoints = await azimuth.azimuth.getUnspawnedChildren(
340− _contracts,
316+ _contracts,
341− curPoint
317+ curPoint
342− );
318+ );
343− }
344
319
345 const outgoingPoints = getOutgoingPoints(
320 const outgoingPoints = getOutgoingPoints(
346 controlledPoints,
321 controlledPoints,
347 getDetails
322 getDetails
348− );
323+ ).filter((p: number) => isPlanet(p) && availablePoints.includes(p));
349
324
350− possibleMissingInvites = possibleMissingInvites.concat(
325+ const invitePlanets = spawnedPoints.concat(outgoingPoints);
351− outgoingPoints.filter(
352− (p: number) => isPlanet(p) && availablePoints.includes(p)
353− )
354− );
355
326
356− // Iterate over all spawned and controlled planets
357− // If the planet is not in available invites, generate the ticket and add it
358 if (isDevelopment) {
327 if (isDevelopment) {
359− console.log('POSSIBLE MISSING', possibleMissingInvites);
328+ console.log('INVITED PLANETS', invitePlanets);
360 }
329 }
361− // TODO: this can be verym slow, so best to allow the user to manually
330+
362− // reveal the ticket, after checking if the invite is still "active"
331+ const newInvites: Invite[] = [];
363− // (i.e. if the address inferred from the derived inviteWallet corresponds to
332+ // Iterate over all of the stored invites, generating wallet info as necessary
364− // to the address the point has as owner)
365− //
366− for (let i = 0; i < possibleMissingInvites.length; i++) {
333+ for (let i = 0; i < invitePlanets.length; i++) {
334+ setInviteGeneratingNum(i + 1);
367− const planet = possibleMissingInvites[i];
335+ const planet = invitePlanets[i];
368− if (!availableInvites.find(hasPoint(planet))) {
336+ const invite = storedInvites.find(hasInvite(planet));
337+
338+ const start = page * INVITES_PER_PAGE;
339+ const end = start + INVITES_PER_PAGE;
340+
341+ if (invite) {
342+ newInvites.push(invite);
343+ } else if (getAll || (i >= start && i < end)) {
344+ if (isDevelopment) {
369− console.log('MISSING IN AVAILABLE', planet);
345+ console.log('MISSING INVITE INFO', planet);
346+ }
370 const { ticket, inviteWallet } = await generateInviteWallet(
347 const { ticket, inviteWallet } = await generateInviteWallet(
371 planet,
348 planet,
372 _authToken
349 _authToken
373 );
350 );
374
351
375− availableInvites.push({
352+ newInvites.push(
353+ inviteTemplate(
354+ planet,
376− ticket,
355+ ticket,
377− status: 'available',
378− planet,
379− hash: '',
380− owner: inviteWallet.ownership.keys.address,
356+ inviteWallet.ownership.keys.address
381− });
357+ )
358+ );
359+ } else {
360+ newInvites.push(inviteTemplate(planet));
382 }
361 }
383 }
362 }
384
363
385− setStoredInvites(curPoint, {
364+ setStoredInvites(curPoint, newInvites);
386− available: availableInvites.filter(({ planet }) =>
387− possibleMissingInvites.includes(planet)
388− ),
389− pending: stillPending,
390− });
391− setInvites(availableInvites);
365+ setInvites(newInvites);
366+
367+ return newInvites;
392 }
368 }
393 } catch (error) {
369 } catch (error) {
394 console.warn('ERROR GETTING INVITES', error);
370 console.warn('ERROR GETTING INVITES', error);
@@ -401,8 +377,48 @@ export default function useRoller() {
401 controlledPoints,
377 controlledPoints,
402 getDetails,
378 getDetails,
403 pointCursor,
379 pointCursor,
380+ invitePoints,
404 setInvites,
381 setInvites,
405− setPendingTransactions,
382+ setInviteGeneratingNum,
383+ ]
384+ );
385+
386+ const getNumInvites = useCallback(
387+ async (isL2: boolean) => {
388+ try {
389+ const curPoint: number = Number(need.point(pointCursor));
390+ const _authToken = authToken.getOrElse(null);
391+ const _contracts = contracts.getOrElse(null);
392+
393+ if (_authToken && _contracts) {
394+ let invitePoints = isL2 ? await api.getSpawned(curPoint) : [];
395+
396+ const availablePoints = await azimuth.azimuth.getUnspawnedChildren(
397+ _contracts,
398+ curPoint
399+ );
400+
401+ const outgoingPoints = getOutgoingPoints(
402+ controlledPoints,
403+ getDetails
404+ ).filter((p: number) => isPlanet(p) && availablePoints.includes(p));
405+
406+ setInvitePoints(invitePoints.concat(outgoingPoints));
407+ }
408+ } catch (e) {
409+ if (isDevelopment) {
410+ console.warn(e);
411+ }
412+ }
413+ },
414+ [
415+ api,
416+ pointCursor,
417+ setInvitePoints,
418+ authToken,
419+ contracts,
420+ controlledPoints,
421+ getDetails,
406 ]
422 ]
407 );
423 );
408
424
@@ -632,7 +648,7 @@ export default function useRoller() {
632 setNextBatchTime(response.nextBatch);
648 setNextBatchTime(response.nextBatch);
633 });
649 });
634
650
635− getInvites(currentL2);
651+ getInvites(currentL2, 0);
636 }
652 }
637 }, 1000);
653 }, 1000);
638
654
@@ -650,5 +666,6 @@ export default function useRoller() {
650 transferPoint,
666 transferPoint,
651 setProxyAddress,
667 setProxyAddress,
652 configureNetworkingKeys,
668 configureNetworkingKeys,
669+ getNumInvites,
653 };
670 };
654}
671}
src/lib/utils/invite.ts+8
@@ -3,7 +3,15 @@ import * as ob from 'urbit-ob';
3export const generateUrl = (ticket: string, planet: number) => {
3export const generateUrl = (ticket: string, planet: number) => {
4 if (ticket && planet) {
4 if (ticket && planet) {
5 return `urb.it/#${ticket.slice(1)}-${ob.patp(planet).slice(1)}`;
5 return `urb.it/#${ticket.slice(1)}-${ob.patp(planet).slice(1)}`;
6+ } else if (planet) {
7+ return ob.patp(planet);
6 }
8 }
7
9
8 return 'No Ticket or Ship';
10 return 'No Ticket or Ship';
9};
11};
12+
13+export const generateCsvLine = (ticket: string, planet: number) =>
14+ `${ob.patp(planet)},${generateUrl(ticket, planet)}\n`;
15+
16+export const generateCsvName = (defaultName: string, point: number) =>
17+ `${ob.patp(point).slice(1)}_${defaultName}`;
src/lib/utils/roller.ts+5−3
@@ -112,15 +112,14 @@ export const configureKeys = async (
112export const transferPointRequest = async (
112export const transferPointRequest = async (
113 api: RollerRPCAPI,
113 api: RollerRPCAPI,
114 _wallet: any,
114 _wallet: any,
115− _point: Ship,
115+ pointToTransfer: Ship,
116− planet: number,
117 proxy: string,
116 proxy: string,
118 nonce: number,
117 nonce: number,
119 address: string,
118 address: string,
120 reset?: boolean
119 reset?: boolean
121) => {
120) => {
122 const from = {
121 const from = {
123− ship: planet, //planet to transfer
122+ ship: pointToTransfer,
124 proxy,
123 proxy,
125 };
124 };
126
125
@@ -198,3 +197,6 @@ export const registerProxyAddress = async (
198};
197};
199
198
200export const reticketL2Point = async () => {};
199export const reticketL2Point = async () => {};
200+
201+export const hasInvite = (point: number) => (invite: Invite) =>
202+ invite.planet === point && invite.ticket.length > 0;
src/store/lib/useDetailsStore.ts+15−8
@@ -6,6 +6,7 @@ import useRoller from '../../lib/useRoller';
6import { useNetwork } from '../network';
6import { useNetwork } from '../network';
7import { L1Point } from 'lib/types/L1Point';
7import { L1Point } from 'lib/types/L1Point';
8import { L2Point } from '@urbit/roller-api';
8import { L2Point } from '@urbit/roller-api';
9+import { isDevelopment } from 'lib/flags';
9
10
10const toL1Details = (point: L2Point): L1Point => {
11const toL1Details = (point: L2Point): L1Point => {
11 return {
12 return {
@@ -57,14 +58,20 @@ export default function useDetailsStore() {
57 }
58 }
58
59
59 // fetch point details
60 // fetch point details
61+ try {
60− const l2Point = await api.getPoint(point);
62+ const l2Point = await api.getPoint(point);
61− const details =
63+ const details =
62− l2Point && (l2Point.dominion === 'l2' || l2Point.dominion === 'spawn')
64+ l2Point && (l2Point.dominion === 'l2' || l2Point.dominion === 'spawn')
63− ? toL1Details(l2Point)
65+ ? toL1Details(l2Point)
64− : await azimuth.azimuth.getPoint(_contracts, point);
66+ : await azimuth.azimuth.getPoint(_contracts, point);
65− addToDetails({
67+ addToDetails({
66− [point]: details,
68+ [point]: details,
67− });
69+ });
70+ } catch (e) {
71+ if (isDevelopment) {
72+ console.warn(e);
73+ }
74+ }
68 },
75 },
69 [api, contracts, addToDetails]
76 [api, contracts, addToDetails]
70 );
77 );
src/store/roller.ts+9
@@ -11,13 +11,17 @@ export interface RollerStore {
11 currentPoint: L2Point | null;
11 currentPoint: L2Point | null;
12 currentL2: boolean;
12 currentL2: boolean;
13 currentL2Spawn: boolean;
13 currentL2Spawn: boolean;
14+ invitePoints: number[];
14 invites: Invite[];
15 invites: Invite[];
15 recentlyCompleted: number;
16 recentlyCompleted: number;
17+ inviteGeneratingNum: number;
16 setNextBatchTime: (nextBatchTime: number) => void;
18 setNextBatchTime: (nextBatchTime: number) => void;
17 setNextRoll: (nextRoll: string) => void;
19 setNextRoll: (nextRoll: string) => void;
18 setPendingTransactions: (pendingTransactions: PendingTransaction[]) => void;
20 setPendingTransactions: (pendingTransactions: PendingTransaction[]) => void;
19 setCurrentPoint: (currentPoint: L2Point) => void;
21 setCurrentPoint: (currentPoint: L2Point) => void;
22+ setInvitePoints: (points: number[]) => void;
20 setInvites: (invites: Invite[]) => void;
23 setInvites: (invites: Invite[]) => void;
24+ setInviteGeneratingNum: (numGenerating: number) => void;
21}
25}
22
26
23export const useRollerStore = create<RollerStore>(set => ({
27export const useRollerStore = create<RollerStore>(set => ({
@@ -27,8 +31,10 @@ export const useRollerStore = create<RollerStore>(set => ({
27 currentPoint: null,
31 currentPoint: null,
28 currentL2: false,
32 currentL2: false,
29 currentL2Spawn: false,
33 currentL2Spawn: false,
34+ invitePoints: [],
30 invites: [],
35 invites: [],
31 recentlyCompleted: 0,
36 recentlyCompleted: 0,
37+ inviteGeneratingNum: 0,
32 setNextBatchTime: (nextBatchTime: number) => set(() => ({ nextBatchTime })),
38 setNextBatchTime: (nextBatchTime: number) => set(() => ({ nextBatchTime })),
33 setNextRoll: (nextRoll: string) => set(() => ({ nextRoll })),
39 setNextRoll: (nextRoll: string) => set(() => ({ nextRoll })),
34 setPendingTransactions: (pendingTransactions: PendingTransaction[]) =>
40 setPendingTransactions: (pendingTransactions: PendingTransaction[]) =>
@@ -49,5 +55,8 @@ export const useRollerStore = create<RollerStore>(set => ({
49 currentL2: isL2(currentPoint?.dominion),
55 currentL2: isL2(currentPoint?.dominion),
50 currentL2Spawn: isL2Spawn(currentPoint?.dominion),
56 currentL2Spawn: isL2Spawn(currentPoint?.dominion),
51 })),
57 })),
58+ setInvitePoints: (invitePoints: number[]) => set(() => ({ invitePoints })),
52 setInvites: (invites: Invite[]) => set(() => ({ invites })),
59 setInvites: (invites: Invite[]) => set(() => ({ invites })),
60+ setInviteGeneratingNum: (inviteGeneratingNum: number) =>
61+ set(() => ({ inviteGeneratingNum })),
53}));
62}));
src/store/storage/roller.ts+3−14
@@ -3,12 +3,7 @@ import { Invite } from 'lib/types/Invite';
3const INVITES_KEY = 'invites';
3const INVITES_KEY = 'invites';
4const HIDE_MIGRATION_MESSAGE_KEY = 'hide_migration_message';
4const HIDE_MIGRATION_MESSAGE_KEY = 'hide_migration_message';
5
5
6−export interface StoredInvites {
7− available: Invite[];
8− pending: Invite[];
9−}
10−
11−export const getStoredInvites = (point: number): StoredInvites => {
6+export const getStoredInvites = (point: number): Invite[] => {
12 const invitesString = localStorage.getItem(INVITES_KEY) || '{}';
7 const invitesString = localStorage.getItem(INVITES_KEY) || '{}';
13 const invites = JSON.parse(invitesString);
8 const invites = JSON.parse(invitesString);
14
9
@@ -16,22 +11,16 @@ export const getStoredInvites = (point: number): StoredInvites => {
16 return invites[String(point)];
11 return invites[String(point)];
17 }
12 }
18
13
19− return { available: [], pending: [] };
14+ return [];
20};
15};
21
16
22−export const setStoredInvites = (point: number, newInvites: StoredInvites) => {
17+export const setStoredInvites = (point: number, newInvites: Invite[]) => {
23 const invitesString = localStorage.getItem(INVITES_KEY) || '{}';
18 const invitesString = localStorage.getItem(INVITES_KEY) || '{}';
24 const invites = JSON.parse(invitesString);
19 const invites = JSON.parse(invitesString);
25 invites[Number(point)] = newInvites;
20 invites[Number(point)] = newInvites;
26 localStorage.setItem(INVITES_KEY, JSON.stringify(invites));
21 localStorage.setItem(INVITES_KEY, JSON.stringify(invites));
27};
22};
28
23
29−export const setPendingInvites = (point: number, pendingInvites: Invite[]) => {
30− const invites = getStoredInvites(point);
31− invites.pending.push(...pendingInvites);
32− setStoredInvites(point, invites);
33−};
34−
35export const clearInvitesStorage = () => localStorage.removeItem(INVITES_KEY);
24export const clearInvitesStorage = () => localStorage.removeItem(INVITES_KEY);
36
25
37export const getHideMigrationMessage = () =>
26export const getHideMigrationMessage = () =>
src/views/Invite/Cohort.scss+11−6
@@ -79,7 +79,7 @@
79 padding: 3px 8px;
79 padding: 3px 8px;
80 }
80 }
81
81
82− .copy-invite {
82+ .copy-invite, .ticket-loader {
83 background-color: $washed-gray;
83 background-color: $washed-gray;
84 border-radius: $medium-border-radius;
84 border-radius: $medium-border-radius;
85 width: 32px;
85 width: 32px;
@@ -88,12 +88,17 @@
88 flex-direction: column;
88 flex-direction: column;
89 justify-content: center;
89 justify-content: center;
90 align-items: center;
90 align-items: center;
91+ }
92+
93+ .copy-invite svg {
94+ font-size: 16px;
95+ margin-top: 6px;
96+ margin-right: 4px;
97+ }
91
98
92− svg {
99+ .ticket-loader > svg {
93− font-size: 16px;
100+ width: 16px;
94− margin-top: 6px;
101+ height: 16px;
95− margin-right: 4px;
96− }
97 }
102 }
98 }
103 }
99
104
src/views/Invite/Cohort.js → src/views/Invite/Cohort.tsxrenamed+122−42
@@ -1,8 +1,12 @@
1import React, { useEffect, useCallback, useState } from 'react';
1import React, { useEffect, useCallback, useState } from 'react';
2import { Grid, Button } from 'indigo-react';
2import { Grid, Button } from 'indigo-react';
3−import cn from 'classnames';
4import * as azimuth from 'azimuth-js';
3import * as azimuth from 'azimuth-js';
4+import {
5+ Icon,
6+ LoadingSpinner,
7+ Row,
8+ StatelessTextInput,
5−import { Icon, Row, StatelessTextInput } from '@tlon/indigo-react';
9+} from '@tlon/indigo-react';
6
10
7import { usePointCursor } from 'store/pointCursor';
11import { usePointCursor } from 'store/pointCursor';
8import { usePointCache } from 'store/pointCache';
12import { usePointCache } from 'store/pointCache';
@@ -14,7 +18,11 @@ import { useLocalRouter } from 'lib/LocalRouter';
14import * as need from 'lib/need';
18import * as need from 'lib/need';
15import * as wg from 'lib/walletgen';
19import * as wg from 'lib/walletgen';
16import useRoller from 'lib/useRoller';
20import useRoller from 'lib/useRoller';
21+import {
22+ generateUrl,
23+ generateCsvLine,
24+ generateCsvName,
17−import { generateUrl } from 'lib/utils/invite';
25+} from 'lib/utils/invite';
18
26
19import { useIssueChild } from 'views/Point/IssueChild';
27import { useIssueChild } from 'views/Point/IssueChild';
20import View from 'components/View';
28import View from 'components/View';
@@ -29,26 +37,40 @@ import InlineEthereumTransaction from 'components/InlineEthereumTransaction';
29
37
30import './Cohort.scss';
38import './Cohort.scss';
31import { getStoredInvites } from 'store/storage/roller';
39import { getStoredInvites } from 'store/storage/roller';
40+import {
41+ DEFAULT_NUM_INVITES,
42+ ETH_ZERO_ADDR,
43+ INVITES_PER_PAGE,
44+ DEFAULT_CSV_NAME,
32−import { ETH_ZERO_ADDR } from 'lib/constants';
45+} from 'lib/constants';
33import { isPlanet } from 'lib/utils/point';
46import { isPlanet } from 'lib/utils/point';
34
47
35−const INVITES_PER_PAGE = 7;
48+interface L1Invite {
36−const DEFAULT_NUM_INVITES = 5;
49+ ticket: string;
50+ planet: number;
51+}
37
52
38export default function InviteCohort() {
53export default function InviteCohort() {
39− const { pop } = useLocalRouter();
54+ const { pop }: any = useLocalRouter();
40− const { authToken } = useWallet();
55+ const { authToken }: any = useWallet();
41 const {
56 const {
42 nextRoll,
57 nextRoll,
43 currentL2,
58 currentL2,
44 pendingTransactions,
59 pendingTransactions,
45 invites,
60 invites,
61+ invitePoints,
62+ setInviteGeneratingNum,
46 setInvites,
63 setInvites,
64+ // inviteGeneratingNum,
47 } = useRollerStore();
65 } = useRollerStore();
66+ const {
67+ generateInviteCodes,
48− const { generateInviteCodes, getPendingTransactions } = useRoller();
68+ getPendingTransactions,
69+ getInvites,
70+ } = useRoller();
49− const { pointCursor } = usePointCursor();
71+ const { pointCursor }: any = usePointCursor();
50− const { contracts } = useNetwork();
72+ const { contracts }: any = useNetwork();
51− const { syncControlledPoints } = usePointCache();
73+ const { syncControlledPoints }: any = usePointCache();
52
74
53 const point = need.point(pointCursor);
75 const point = need.point(pointCursor);
54 const _contracts = need.contracts(contracts);
76 const _contracts = need.contracts(contracts);
@@ -58,11 +80,19 @@ export default function InviteCohort() {
58 );
80 );
59 const [showInviteForm, setShowInviteForm] = useState(false);
81 const [showInviteForm, setShowInviteForm] = useState(false);
60 const [loading, setLoading] = useState(false);
82 const [loading, setLoading] = useState(false);
61− const [error, setError] = useState(null);
83+ const [error, setError] = useState('');
84+ const [downloadingCsv, setDownloadingCsv] = useState(false);
62− const [l1Invite, setL1Invite] = useState(null);
85+ const [l1Invite, setL1Invite] = useState<L1Invite | null>(null);
86+ const [page, setPage] = useState(0);
63
87
64 const { construct, unconstruct, completed, bind } = useIssueChild();
88 const { construct, unconstruct, completed, bind } = useIssueChild();
65
89
90+ useEffect(() => {
91+ if (invitePoints.length > 0) {
92+ getInvites(currentL2, page);
93+ }
94+ }, []); // eslint-disable-line react-hooks/exhaustive-deps
95+
66 // Set up the invite spawn if on L1
96 // Set up the invite spawn if on L1
67 useEffect(() => {
97 useEffect(() => {
68 const _authToken = authToken.getOrElse(null);
98 const _authToken = authToken.getOrElse(null);
@@ -119,26 +149,24 @@ export default function InviteCohort() {
119 ]);
149 ]);
120
150
121 useEffect(() => {
151 useEffect(() => {
122− const { available } = getStoredInvites(point);
152+ const storedInvites = getStoredInvites(point);
123
153
124 if (
154 if (
125 !completed ||
155 !completed ||
126 !l1Invite ||
156 !l1Invite ||
127− available.find(({ planet }) => planet === l1Invite.planet)
157+ storedInvites.find(({ planet }) => planet === l1Invite.planet)
128 ) {
158 ) {
129 return;
159 return;
130 }
160 }
131
161
132 syncControlledPoints();
162 syncControlledPoints();
133− setInvites([...invites, l1Invite]);
163+ setInvites([...invites, { ...l1Invite, hash: '', owner: '' }]);
134 setShowInviteForm(false);
164 setShowInviteForm(false);
135 setL1Invite(null);
165 setL1Invite(null);
136 unconstruct();
166 unconstruct();
137 pop();
167 pop();
138 }, [completed]); // eslint-disable-line react-hooks/exhaustive-deps
168 }, [completed]); // eslint-disable-line react-hooks/exhaustive-deps
139
169
140− const [page, setPage] = useState(0);
141−
142 const invitesToDisplay = invites.slice(
170 const invitesToDisplay = invites.slice(
143 page * INVITES_PER_PAGE,
171 page * INVITES_PER_PAGE,
144 (page + 1) * INVITES_PER_PAGE
172 (page + 1) * INVITES_PER_PAGE
@@ -151,16 +179,64 @@ export default function InviteCohort() {
151 setLoading(true);
179 setLoading(true);
152 try {
180 try {
153 await generateInviteCodes(numInvites);
181 await generateInviteCodes(numInvites);
154− getPendingTransactions(point);
182+ getPendingTransactions();
155 pop();
183 pop();
156 } catch (error) {
184 } catch (error) {
157 console.log(error);
185 console.log(error);
158 // TODO: error is not used anywhere on the UI
186 // TODO: error is not used anywhere on the UI
159− setError(error);
187+ setError(error as string);
160 } finally {
188 } finally {
161 setLoading(false);
189 setLoading(false);
190+ setInviteGeneratingNum(0);
162 }
191 }
192+ }, [
193+ generateInviteCodes,
194+ numInvites,
163− }, [generateInviteCodes, numInvites, getPendingTransactions, point, pop]);
195+ getPendingTransactions,
196+ pop,
197+ setInviteGeneratingNum,
198+ ]);
199+
200+ const downloadCsv = useCallback(() => {
201+ const generateAndDownload = async () => {
202+ setDownloadingCsv(true);
203+ const invites = await getInvites(currentL2, page, true);
204+ if (invites) {
205+ const csv = invites.reduce(
206+ (csvData, { ticket, planet }) =>
207+ (csvData += generateCsvLine(ticket, planet)),
208+ 'Planet,Invite URL\n'
209+ );
210+ const hiddenElement = document.createElement('a');
211+ hiddenElement.href = `data:text/csv;charset=utf-8,${encodeURIComponent(
212+ csv
213+ )}`;
214+ hiddenElement.target = '_blank';
215+
216+ //provide the name for the CSV file to be downloaded
217+ hiddenElement.download = generateCsvName(DEFAULT_CSV_NAME, point);
218+ hiddenElement.click();
219+ } else {
220+ setError('There was an error creating the CSV');
221+ }
222+ setDownloadingCsv(false);
223+ setInviteGeneratingNum(0);
224+ };
225+ generateAndDownload();
226+ }, [
227+ getInvites,
228+ setError,
229+ setDownloadingCsv,
230+ setInviteGeneratingNum,
231+ currentL2,
232+ page,
233+ point,
234+ ]);
235+
236+ const goNextPage = useCallback(() => {
237+ setPage(page + 1);
238+ getInvites(currentL2, page + 1);
239+ }, [currentL2, getInvites, page]);
164
240
165 const getContent = () => {
241 const getContent = () => {
166 if (hasInvites) {
242 if (hasInvites) {
@@ -175,14 +251,20 @@ export default function InviteCohort() {
175 </div>
251 </div>
176 <div className="invites">
252 <div className="invites">
177 {invitesToDisplay.map(invite => (
253 {invitesToDisplay.map(invite => (
178− <div className="invite" key={invite.ticket}>
254+ <div className="invite" key={invite.planet}>
179 <div className="invite-url">
255 <div className="invite-url">
180 {generateUrl(invite.ticket, invite.planet)}
256 {generateUrl(invite.ticket, invite.planet)}
181 </div>
257 </div>
258+ {invite.ticket ? (
182− <CopiableWithTooltip
259+ <CopiableWithTooltip
183− text={generateUrl(invite.ticket, invite.planet)}
260+ text={generateUrl(invite.ticket, invite.planet)}
184− className={cn('copy-invite')}
261+ className="copy-invite"
185− />
262+ />
263+ ) : (
264+ <div className="ticket-loader">
265+ <LoadingSpinner foreground="black" background="white" />
266+ </div>
267+ )}
186 </div>
268 </div>
187 ))}
269 ))}
188 </div>
270 </div>
@@ -226,6 +308,7 @@ export default function InviteCohort() {
226 if (showInviteForm) {
308 if (showInviteForm) {
227 return (
309 return (
228 <View
310 <View
311+ pop={() => setShowInviteForm(false)}
229 className="cohort show-invite-form"
312 className="cohort show-invite-form"
230 hideBack
313 hideBack
231 header={<L2BackHeader back={() => setShowInviteForm(false)} />}>
314 header={<L2BackHeader back={() => setShowInviteForm(false)} />}>
@@ -258,15 +341,6 @@ export default function InviteCohort() {
258 </span>
341 </span>
259 </div>
342 </div>
260 )}
343 )}
261− {/* <div>Destination</div>
262− <div className="ship-or-address">Ship or ethereum address</div>
263− <Dropdown value={dropdownValue}>
264− {points.map(point => (
265− <div onClick={selectPoint(point)} className="address">
266− {ob.patp(point)}
267− </div>
268− ))}
269− </Dropdown> */}
270 </div>
344 </div>
271 {/* <Inviter /> */}
345 {/* <Inviter /> */}
272 <div className="lower">
346 <div className="lower">
@@ -285,7 +359,7 @@ export default function InviteCohort() {
285 disabled={loading}
359 disabled={loading}
286 onClick={createInvites}>
360 onClick={createInvites}>
287 {loading
361 {loading
288− ? 'Generating...'
362+ ? `Generating ${numInvites} invites...`
289 : `Generate Planet Code${
363 : `Generate Planet Code${
290 currentL2 ? `s (${numInvites})` : ''
364 currentL2 ? `s (${numInvites})` : ''
291 }`}
365 }`}
@@ -323,10 +397,16 @@ export default function InviteCohort() {
323 ) : (
397 ) : (
324 <Row className="has-invites-header">
398 <Row className="has-invites-header">
325 {header}
399 {header}
400+ {downloadingCsv ? (
401+ <Row className="download-csv">
402+ Generating {invitePoints.length} codes...
403+ </Row>
404+ ) : (
326− <div className="download-csv" onClick={() => null}>
405+ <Row className="download-csv" onClick={downloadCsv}>
327− <Icon icon="Download" />
406+ <Icon icon="Download" />
328− <div>CSV</div>
407+ <div>CSV</div>
329− </div>
408+ </Row>
409+ )}
330 </Row>
410 </Row>
331 )}
411 )}
332 </HeaderPane>
412 </HeaderPane>
@@ -350,7 +430,7 @@ export default function InviteCohort() {
350 numPerPage={INVITES_PER_PAGE}
430 numPerPage={INVITES_PER_PAGE}
351 numElements={invites.length}
431 numElements={invites.length}
352 goPrevious={() => setPage(page - 1)}
432 goPrevious={() => setPage(page - 1)}
353− goNext={() => setPage(page + 1)}
433+ goNext={goNextPage}
354 />
434 />
355 )}
435 )}
356 </BodyPane>
436 </BodyPane>
src/views/Point/Point.tsx+19−14
@@ -1,4 +1,4 @@
1−import { useCallback, useEffect, useMemo, useState } from 'react';
1+import { useCallback, useEffect, useState } from 'react';
2import { azimuth } from 'azimuth-js';
2import { azimuth } from 'azimuth-js';
3import { Just } from 'folktale/maybe';
3import { Just } from 'folktale/maybe';
4import { Grid, Flex } from 'indigo-react';
4import { Grid, Flex } from 'indigo-react';
@@ -34,17 +34,18 @@ import { useHasNetworkKeysSet } from 'lib/useHasNetworkKeysSet';
34import { InviteForm } from './InviteForm';
34import { InviteForm } from './InviteForm';
35
35
36export default function Point() {
36export default function Point() {
37− const { pop, push, names } = useLocalRouter();
37+ const { pop, push, names }: any = useLocalRouter();
38− const { wallet } = useWallet();
38+ const { wallet }: any = useWallet();
39− const { pointCursor } = usePointCursor();
39+ const { pointCursor }: any = usePointCursor();
40 const point = need.point(pointCursor);
40 const point = need.point(pointCursor);
41− const { syncExtras } = usePointCache();
41+ const { syncExtras }: any = usePointCache();
42− const { api, getInvites } = useRoller();
42+ const { api, getNumInvites, getPendingTransactions } = useRoller();
43 const {
43 const {
44 pendingTransactions,
44 pendingTransactions,
45 nextRoll,
45 nextRoll,
46− invites,
46+ invitePoints,
47 setCurrentPoint,
47 setCurrentPoint,
48+ setInvites,
48 } = useRollerStore();
49 } = useRollerStore();
49 const networkKeysSet = useHasNetworkKeysSet();
50 const networkKeysSet = useHasNetworkKeysSet();
50 const [showModal, setShowModal] = useState(false);
51 const [showModal, setShowModal] = useState(false);
@@ -66,18 +67,22 @@ export default function Point() {
66 if (isDevelopment) {
67 if (isDevelopment) {
67 console.log('POINT INFO', pointInfo);
68 console.log('POINT INFO', pointInfo);
68 }
69 }
70+ getPendingTransactions();
69 setCurrentPoint(pointInfo);
71 setCurrentPoint(pointInfo);
70− await getInvites(isL2(pointInfo.dominion));
72+ getNumInvites(isL2(pointInfo.dominion));
71 };
73 };
72
74
73 await getTransactions();
75 await getTransactions();
74− }, [api, point, getInvites, setCurrentPoint]);
76+ }, [api, point, getNumInvites, setCurrentPoint, getPendingTransactions]);
75
77
76 useEffect(() => {
78 useEffect(() => {
77 loadL1Info();
79 loadL1Info();
78 loadL2Info();
80 loadL2Info();
81+ }, [point]); // eslint-disable-line react-hooks/exhaustive-deps
82+
83+ useEffect(() => {
84+ setInvites([]);
79− // eslint-disable-next-line react-hooks/exhaustive-deps
85+ }, []); // eslint-disable-line react-hooks/exhaustive-deps
80− }, [point]);
81
86
82 const { isParent, canManage, canSpawn, canVote } = useCurrentPermissions();
87 const { isParent, canManage, canSpawn, canVote } = useCurrentPermissions();
83
88
@@ -156,7 +161,7 @@ export default function Point() {
156 header={
161 header={
157 <L2PointHeader
162 <L2PointHeader
158 hideTimer={!!spawnedPending}
163 hideTimer={!!spawnedPending}
159− numInvites={invites.length}
164+ numInvites={invitePoints.length}
160 hideInvites={!networkKeysSet}
165 hideInvites={!networkKeysSet}
161 />
166 />
162 }>
167 }>
@@ -244,7 +249,7 @@ export default function Point() {
244 <Card
249 <Card
245 title="ID"
250 title="ID"
246 subtitle="Identity and security settings"
251 subtitle="Identity and security settings"
247− icon={<Icon icon="User" />}
252+ icon={(<Icon icon="User" />) as any}
248 onClick={goUrbitID}
253 onClick={goUrbitID}
249 disabled={!canManage}
254 disabled={!canManage}
250 />
255 />
@@ -253,7 +258,7 @@ export default function Point() {
253 <Card
258 <Card
254 title="OS"
259 title="OS"
255 subtitle="Urbit OS Settings"
260 subtitle="Urbit OS Settings"
256− icon={<Icon icon="Server" />}
261+ icon={(<Icon icon="Server" />) as any}
257 onClick={goUrbitOS}
262 onClick={goUrbitOS}
258 disabled={!canManage}
263 disabled={!canManage}
259 />
264 />
src/views/Point/UrbitID.js+12−39
@@ -1,15 +1,12 @@
1import React from 'react';
1import React from 'react';
2−import { Grid } from 'indigo-react';
3
2
4import { useHistory } from 'store/history';
3import { useHistory } from 'store/history';
5
4
6−import useCurrentPointName from 'lib/useCurrentPointName';
7import useRouter from 'lib/useRouter';
5import useRouter from 'lib/useRouter';
8import { LocalRouterProvider } from 'lib/LocalRouter';
6import { LocalRouterProvider } from 'lib/LocalRouter';
9
7
10import View from 'components/View';
8import View from 'components/View';
11−import Crumbs from 'components/Crumbs';
9+import L2BackHeader from 'components/L2/Headers/L2BackHeader';
12−import NavHeader from 'components/NavHeader';
13
10
14import UrbitIDHome from '../UrbitID/Home';
11import UrbitIDHome from '../UrbitID/Home';
15import SigilGenerator from '../UrbitID/SigilGenerator';
12import SigilGenerator from '../UrbitID/SigilGenerator';
@@ -36,26 +33,8 @@ const VIEWS = {
36 [NAMES.TRANSFER]: Transfer,
33 [NAMES.TRANSFER]: Transfer,
37};
34};
38
35
39−const humanizeName = name => {
40− switch (name) {
41− case NAMES.SIGIL_GENERATOR:
42− return 'Sigil';
43− case NAMES.DOWNLOAD_KEYS:
44− return 'Download Keys';
45− case NAMES.SET_PROXY:
46− return 'Set Key';
47− case NAMES.RESET_KEYS:
48− return 'Reset Keys';
49− case NAMES.TRANSFER:
50− return 'Transfer';
51− default:
52− return undefined;
53− }
54−};
55−
56export default function UrbitID() {
36export default function UrbitID() {
57 const history = useHistory();
37 const history = useHistory();
58− const name = useCurrentPointName();
59
38
60 const { Route, ...router } = useRouter({
39 const { Route, ...router } = useRouter({
61 names: NAMES,
40 names: NAMES,
@@ -63,28 +42,22 @@ export default function UrbitID() {
63 initialRoutes: [{ key: NAMES.HOME }],
42 initialRoutes: [{ key: NAMES.HOME }],
64 });
43 });
65
44
66− const last = router.peek().key;
45+ const onBack = () =>
67− const homeAction = last === NAMES.HOME ? undefined : () => history.pop();
46+ router.peek().key === NAMES.HOME ? history.pop() : router.pop();
68− const lastCrumb = homeAction ? [{ text: humanizeName(last) }] : [];
69
47
70 return (
48 return (
71 <View
49 <View
72− pop={() =>
50+ pop={onBack}
51+ inset
52+ hideBack
53+ header={
54+ <L2BackHeader
73− router.peek().key === NAMES.HOME ? history.pop() : router.pop()
55+ hideBalance={router.peek().key === NAMES.HOME}
74− }
56+ back={onBack}
75− inset>
76− <NavHeader>
77− <Crumbs
78− routes={[
79− { text: name, action: () => history.pop() },
80− { text: 'Urbit ID', action: homeAction },
81− ...lastCrumb,
82− ]}
83 />
57 />
84− </NavHeader>
58+ }
85−
59+ className="urbit-id">
86 <LocalRouterProvider value={router}>
60 <LocalRouterProvider value={router}>
87− <Grid className="mb4"></Grid>
88 <Route />
61 <Route />
89 </LocalRouterProvider>
62 </LocalRouterProvider>
90 </View>
63 </View>
src/views/Point/UrbitOS.js+13−17
@@ -1,15 +1,12 @@
1import React, { useState } from 'react';
1import React, { useState } from 'react';
2−import { Grid } from 'indigo-react';
3
2
4import { useHistory } from 'store/history';
3import { useHistory } from 'store/history';
5
4
6−import useCurrentPointName from 'lib/useCurrentPointName';
7import useRouter from 'lib/useRouter';
5import useRouter from 'lib/useRouter';
8import { LocalRouterProvider } from 'lib/LocalRouter';
6import { LocalRouterProvider } from 'lib/LocalRouter';
9
7
10import View from 'components/View';
8import View from 'components/View';
11−import Crumbs from 'components/Crumbs';
9+import L2BackHeader from 'components/L2/Headers/L2BackHeader';
12−import NavHeader from 'components/NavHeader';
13
10
14import UrbitOSHome from '../UrbitOS/Home';
11import UrbitOSHome from '../UrbitOS/Home';
15import UrbitOSNetworkingKeys from '../UrbitOS/NetworkingKeys';
12import UrbitOSNetworkingKeys from '../UrbitOS/NetworkingKeys';
@@ -29,7 +26,6 @@ const VIEWS = {
29
26
30export default function UrbitOS() {
27export default function UrbitOS() {
31 const history = useHistory();
28 const history = useHistory();
32− const name = useCurrentPointName();
33
29
34 const { Route, ...router } = useRouter({
30 const { Route, ...router } = useRouter({
35 names: NAMES,
31 names: NAMES,
@@ -39,22 +35,22 @@ export default function UrbitOS() {
39
35
40 const [manualNetworkSeed, setManualNetworkSeed] = useState();
36 const [manualNetworkSeed, setManualNetworkSeed] = useState();
41
37
38+ const onBack = () =>
39+ router.peek().key === NAMES.HOME ? history.pop() : router.pop();
40+
42 return (
41 return (
43 <View
42 <View
44− pop={() =>
43+ pop={onBack}
44+ inset
45+ hideBack
46+ header={
47+ <L2BackHeader
45− router.peek().key === NAMES.HOME ? history.pop() : router.pop()
48+ hideBalance={router.peek().key === NAMES.HOME}
46− }
49+ back={onBack}
47− inset>
48− <NavHeader>
49− <Crumbs
50− routes={[
51− { text: name, action: () => history.pop() },
52− { text: 'Urbit OS' },
53− ]}
54 />
50 />
55− </NavHeader>
51+ }
52+ className="urbit-id">
56 <LocalRouterProvider value={router}>
53 <LocalRouterProvider value={router}>
57− <Grid className="mb4"></Grid>
58 <Route
54 <Route
59 manualNetworkSeed={manualNetworkSeed}
55 manualNetworkSeed={manualNetworkSeed}
60 setManualNetworkSeed={setManualNetworkSeed}
56 setManualNetworkSeed={setManualNetworkSeed}
src/views/UrbitID/Home.jsdeleted−167
@@ -1,167 +0,0 @@
1−import React, { useCallback } from 'react';
2−import { Grid, B } from 'indigo-react';
3−import { azimuth } from 'azimuth-js';
4−import { Just } from 'folktale/maybe';
5−
6−import { ForwardButton } from 'components/Buttons';
7−import CopyButton from 'components/copiable/CopyButton';
8−
9−import { useLocalRouter } from 'lib/LocalRouter';
10−import { PROXY_TYPE, proxyTypeToHuman } from 'lib/proxy';
11−import * as need from 'lib/need';
12−import { capitalize } from 'lib/capitalize';
13−import useCurrentPermissions from 'lib/useCurrentPermissions';
14−import { convertToInt } from 'lib/convertToInt';
15−
16−import { usePointCursor } from 'store/pointCursor';
17−import { usePointCache } from 'store/pointCache';
18−import { useWallet } from 'store/wallet';
19−
20−export default function UrbitIDHome() {
21− const { push, names } = useLocalRouter();
22− const { pointCursor } = usePointCursor();
23− const { getDetails } = usePointCache();
24− const { urbitWallet } = useWallet();
25−
26− const goSigil = useCallback(() => push(names.SIGIL_GENERATOR), [push, names]);
27−
28− const goDownloadKeys = useCallback(() => push(names.DOWNLOAD_KEYS), [
29− push,
30− names,
31− ]);
32−
33− const goUrbitOS = useCallback(() => push(names.URBIT_OS), [push, names]);
34−
35− const goResetKeys = useCallback(() => push(names.RESET_KEYS), [push, names]);
36−
37− const isMasterTicket = Just.hasInstance(urbitWallet);
38− const point = need.point(pointCursor);
39− const details = need.details(getDetails(point));
40− const { isOwner } = useCurrentPermissions();
41− const networkRevision = convertToInt(details.keyRevisionNumber, 10);
42−
43− const pointSize = azimuth.getPointSize(point);
44− const isParent = pointSize !== azimuth.PointSize.Planet;
45− const isSenate = pointSize === azimuth.PointSize.Galaxy;
46−
47− const goSetProxy = useCallback(
48− proxyType => push(names.SET_PROXY, { proxyType }),
49− [push, names]
50− );
51−
52− const goTransfer = useCallback(() => push(names.TRANSFER), [names, push]);
53−
54− const renderProxyAction = useCallback(
55− (proxyType, address) => {
56− const disabled =
57− !isOwner ||
58− (proxyType === PROXY_TYPE.SPAWN ? networkRevision === 0 : false);
59− return (
60− <>
61− <Grid.Item
62− full
63− as={ForwardButton}
64− disabled={disabled}
65− onClick={() => goSetProxy(proxyType)}>
66− Edit {capitalize(proxyTypeToHuman(proxyType))} Key
67− </Grid.Item>
68− </>
69− );
70− },
71− [goSetProxy, isOwner, networkRevision]
72− );
73− return (
74− <Grid>
75− <Grid.Item full className="mb7 f5">
76− Urbit ID
77− </Grid.Item>
78− <Grid.Divider />
79− <Grid.Item full as={ForwardButton} onClick={goSigil}>
80− Sigil
81− </Grid.Item>
82− <Grid.Divider />
83− <Grid.Item
84− full
85− as={ForwardButton}
86− onClick={goDownloadKeys}
87− disabled={!isMasterTicket}
88− disabledDetail={
89− <B className="wrap ws-normal">
90− · Master Key Authentication required.
91− </B>
92− }>
93− Download Keys
94− </Grid.Item>
95− <Grid.Divider />
96− <Grid.Item
97− full
98− as={ForwardButton}
99− onClick={goResetKeys}
100− disabled={!isOwner}
101− disabledDetail={
102− <B className="wrap ws-normal"> · Ownership key required</B>
103− }
104− detail={
105− isMasterTicket
106− ? 'Reset Master Key and all other keys'
107− : 'Transfer to Master Ticket, resetting all keys'
108− }>
109− {isMasterTicket ? 'Reset Keys' : 'Transfer to Master Ticket'}
110− </Grid.Item>
111−
112− <Grid.Divider />
113−
114− <Grid.Item full className="mv7 f5 gray4">
115− Advanced
116− </Grid.Item>
117− <Grid.Divider />
118− <Grid.Item
119− full
120− as={ForwardButton}
121− accessory={<CopyButton text={details.owner} />}
122− detail={<div className="mt1 mono black">{details.owner}</div>}
123− disabled={!details.owner}>
124− Ownership Address
125− </Grid.Item>
126− <Grid.Divider />
127−
128− {renderProxyAction(PROXY_TYPE.MANAGEMENT, details.managementProxy)}
129− <Grid.Divider />
130−
131− {isParent && renderProxyAction(PROXY_TYPE.SPAWN, details.spawnProxy)}
132−
133− {isParent && networkRevision !== 0 && <Grid.Divider />}
134−
135− {isParent && networkRevision === 0 && (
136− <>
137− <Grid.Item
138− full
139− as={ForwardButton}
140− onClick={goUrbitOS}
141− className="f6"
142− accessory={<span className="underline pointer">Set Keys</span>}>
143− Network Keys Required
144− </Grid.Item>
145− <Grid.Divider />
146− </>
147− )}
148−
149− {isSenate && (
150− <>
151− {renderProxyAction(PROXY_TYPE.VOTING, details.votingProxy)}{' '}
152− <Grid.Divider />
153− </>
154− )}
155− <Grid.Item
156− full
157− as={ForwardButton}
158− onClick={goTransfer}
159− disabled={!isOwner}
160− disabledDetail={
161− <B className="wrap ws-normal"> · Ownership key required</B>
162− }>
163− Transfer this point
164− </Grid.Item>
165− </Grid>
166− );
167−}
src/views/UrbitID/Home.tsxnew file+212
@@ -0,0 +1,212 @@
1+import React, { useCallback } from 'react';
2+import { azimuth } from 'azimuth-js';
3+import { Just } from 'folktale/maybe';
4+import { Box, Button, Icon, Row } from '@tlon/indigo-react';
5+
6+import { useLocalRouter } from 'lib/LocalRouter';
7+import { PROXY_TYPE } from 'lib/proxy';
8+import * as need from 'lib/need';
9+import useCurrentPermissions from 'lib/useCurrentPermissions';
10+import { convertToInt } from 'lib/convertToInt';
11+import { ETH_ZERO_ADDR } from 'lib/constants';
12+import { abbreviateAddress } from 'lib/utils/address';
13+
14+import { usePointCursor } from 'store/pointCursor';
15+import { usePointCache } from 'store/pointCache';
16+import { useWallet } from 'store/wallet';
17+import { useRollerStore } from 'store/roller';
18+
19+import Window from 'components/L2/Window/Window';
20+import HeaderPane from 'components/L2/Window/HeaderPane';
21+import BodyPane from 'components/L2/Window/BodyPane';
22+import CopiableWithTooltip from 'components/copiable/CopiableWithTooltip';
23+
24+import './UrbitID.scss';
25+
26+export default function UrbitIDHome() {
27+ const { push, names }: any = useLocalRouter();
28+ const { pointCursor }: any = usePointCursor();
29+ const { getDetails }: any = usePointCache();
30+ const { urbitWallet }: any = useWallet();
31+ const { currentPoint } = useRollerStore();
32+
33+ const goSigil = useCallback(() => push(names.SIGIL_GENERATOR), [push, names]);
34+
35+ const goDownloadKeys = useCallback(() => push(names.DOWNLOAD_KEYS), [
36+ push,
37+ names,
38+ ]);
39+
40+ const goUrbitOS = useCallback(() => push(names.URBIT_OS), [push, names]);
41+
42+ const goResetKeys = useCallback(() => push(names.RESET_KEYS), [push, names]);
43+
44+ const isMasterTicket = Just.hasInstance(urbitWallet);
45+ const point = need.point(pointCursor);
46+ const details = need.details(getDetails(point));
47+ const { isOwner } = useCurrentPermissions();
48+ const networkRevision = convertToInt(details.keyRevisionNumber, 10);
49+
50+ const pointSize = azimuth.getPointSize(point);
51+ const isParent = pointSize !== azimuth.PointSize.Planet;
52+ const isSenate = pointSize === azimuth.PointSize.Galaxy;
53+
54+ const goSetProxy = useCallback(
55+ proxyType => push(names.SET_PROXY, { proxyType }),
56+ [push, names]
57+ );
58+
59+ const goTransfer = useCallback(() => push(names.TRANSFER), [names, push]);
60+
61+ const noManagement = details.managementProxy === ETH_ZERO_ADDR;
62+ const noSpawn = details.spawnProxy === ETH_ZERO_ADDR;
63+ const showSpawn = currentPoint?.dominion === 'l1';
64+ const showManage = showSpawn || currentPoint?.dominion === 'spawn';
65+
66+ return (
67+ <Window className="id-home">
68+ <HeaderPane>
69+ <Row className="header-row">
70+ <h5>ID</h5>
71+ <Button className="header-button">Download Passport</Button>
72+ </Row>
73+ </HeaderPane>
74+ <BodyPane>
75+ {isOwner && (
76+ <Row className="between-row owner">
77+ <Box>
78+ <Box>Ownership Address</Box>
79+ <div className="mt1 mono black subtitle">
80+ {abbreviateAddress(details.owner)}
81+ </div>
82+ </Box>
83+ <Row>
84+ <Button className="secondary" onClick={goTransfer}>
85+ Transfer
86+ </Button>
87+ <CopiableWithTooltip
88+ text={details.owner}
89+ className="copy-button"
90+ />
91+ </Row>
92+ </Row>
93+ )}
94+
95+ {isOwner && showManage && (
96+ <Row className="between-row management">
97+ <Box>
98+ <Box>Management Address</Box>
99+ <div className="mt1 mono black subtitle">
100+ {noManagement
101+ ? 'Unset'
102+ : abbreviateAddress(details.managementProxy)}
103+ </div>
104+ </Box>
105+ <Row>
106+ <Button
107+ className="secondary"
108+ onClick={() => goSetProxy(PROXY_TYPE.MANAGEMENT)}>
109+ {noManagement ? 'Set' : 'Change'}
110+ </Button>
111+ {!noManagement && (
112+ <CopiableWithTooltip
113+ text={details.managementProxy}
114+ className="copy-button"
115+ />
116+ )}
117+ </Row>
118+ </Row>
119+ )}
120+
121+ {isParent && networkRevision !== 0 && showSpawn && (
122+ <Row className="between-row management">
123+ <Box>
124+ <Box>Spawn Proxy Address</Box>
125+ <div className="mt1 mono black subtitle">
126+ {noSpawn ? 'Unset' : abbreviateAddress(details.spawnProxy)}
127+ </div>
128+ </Box>
129+ <Row>
130+ <Button
131+ className="secondary"
132+ onClick={() => goSetProxy(PROXY_TYPE.SPAWN)}>
133+ {noSpawn ? 'Set' : 'Change'}
134+ </Button>
135+ {!noSpawn && (
136+ <CopiableWithTooltip
137+ text={details.spawnProxy}
138+ className="copy-button"
139+ />
140+ )}
141+ </Row>
142+ </Row>
143+ )}
144+
145+ {isOwner && (
146+ <Row className="between-row management">
147+ <Box>
148+ <Box>Master Ticket</Box>
149+ <Box className="subtitle">
150+ {isMasterTicket
151+ ? 'Reset Master Key and all other keys'
152+ : 'Transfer to Master Ticket, resetting all keys'}
153+ </Box>
154+ </Box>
155+ <Button className="secondary" onClick={goResetKeys}>
156+ {isMasterTicket ? 'Reset' : 'Transfer'}
157+ </Button>
158+ </Row>
159+ )}
160+
161+ {isParent && networkRevision === 0 && (
162+ <Row className="between-row management">
163+ <Box>
164+ <Box>Network Keys Required</Box>
165+ <Box className="subtitle">
166+ Set your network keys to spawn points
167+ </Box>
168+ </Box>
169+ <Button className="secondary" onClick={goUrbitOS}>
170+ Set Keys
171+ </Button>
172+ </Row>
173+ )}
174+
175+ {isMasterTicket && (
176+ <Row className="between-row management">
177+ <Box>
178+ <Box>Download Keys</Box>
179+ <Box className="subtitle">
180+ Download the keys to your master ticket.
181+ </Box>
182+ </Box>
183+ <Button className="secondary" onClick={goDownloadKeys}>
184+ Download
185+ </Button>
186+ </Row>
187+ )}
188+
189+ <Row className="between-row sigil" onClick={goSigil}>
190+ <Box>
191+ <Box>Sigil</Box>
192+ <Box className="subtitle">Modify and download your sigil</Box>
193+ </Box>
194+ <Icon className="arrow-button" icon="ArrowEast" />
195+ </Row>
196+
197+ {isSenate && (
198+ <Row className="between-row management">
199+ <Box>
200+ <Box>Edit Voting Key</Box>
201+ </Box>
202+ <Button
203+ className="secondary"
204+ onClick={() => goSetProxy(PROXY_TYPE.VOTING)}>
205+ Edit
206+ </Button>
207+ </Row>
208+ )}
209+ </BodyPane>
210+ </Window>
211+ );
212+}
src/views/UrbitID/UrbitID.scssnew file+17
@@ -0,0 +1,17 @@
1+@import 'style/shared/variables.scss';
2+
3+.urbit-id {
4+ font-size: 14px;
5+
6+ .body-pane {
7+ .between-row {
8+ &.sigil {
9+ align-items: flex-start;
10+ &:hover {
11+ cursor: pointer;
12+ opacity: 0.8;
13+ }
14+ }
15+ }
16+ }
17+}
src/views/UrbitOS/Home.tsx+85−56
@@ -8,21 +8,26 @@ import { usePointCache } from 'store/pointCache';
8
8
9import * as need from 'lib/need';
9import * as need from 'lib/need';
10
10
11−import { ForwardButton } from 'components/Buttons';
12import NetworkingKeys from 'components/NetworkingKeys';
11import NetworkingKeys from 'components/NetworkingKeys';
12+import Window from 'components/L2/Window/Window';
13+import HeaderPane from 'components/L2/Window/HeaderPane';
14+import BodyPane from 'components/L2/Window/BodyPane';
15+import { ReactComponent as KeyfileIcon } from 'assets/keyfile.svg';
13
16
14import { useLocalRouter } from 'lib/LocalRouter';
17import { useLocalRouter } from 'lib/LocalRouter';
15import { L1Point } from 'lib/types/L1Point';
18import { L1Point } from 'lib/types/L1Point';
16import AlertBox from 'components/AlertBox';
19import AlertBox from 'components/AlertBox';
17−import DownloadKeyfileButton from 'components/DownloadKeyfileButton';
18import useKeyfileGenerator from 'lib/useKeyfileGenerator';
20import useKeyfileGenerator from 'lib/useKeyfileGenerator';
19−import AccessCode from './AccessCode';
21+import { Box, Button, Row } from '@tlon/indigo-react';
22+import CopiableWithTooltip from 'components/copiable/CopiableWithTooltip';
20
23
21−export default function UrbitOSHome({ manualNetworkSeed }) {
24+import './UrbitOS.scss';
22− const { pointCursor } = usePointCursor();
23− const { getDetails } = usePointCache();
24
25
26+export default function UrbitOSHome() {
27+ const { pointCursor }: any = usePointCursor();
28+ const { getDetails }: any = usePointCache();
29+
25− const { push, names } = useLocalRouter();
30+ const { push, names }: any = useLocalRouter();
26
31
27 const point = need.point(pointCursor);
32 const point = need.point(pointCursor);
28 const details: L1Point = need.details(getDetails(point));
33 const details: L1Point = need.details(getDetails(point));
@@ -36,7 +41,6 @@ export default function UrbitOSHome({ manualNetworkSeed }) {
36
41
37 // The initial key revision number is 0, and increments when set
42 // The initial key revision number is 0, and increments when set
38 const hasSetNetworkingKeys = details.keyRevisionNumber !== '0';
43 const hasSetNetworkingKeys = details.keyRevisionNumber !== '0';
39− const networkKeysAction = hasSetNetworkingKeys ? 'Reset' : 'Initialize';
40
44
41 const goNetworkingKeys = useCallback(() => push(names.NETWORKING_KEYS), [
45 const goNetworkingKeys = useCallback(() => push(names.NETWORKING_KEYS), [
42 names,
46 names,
@@ -51,54 +55,67 @@ export default function UrbitOSHome({ manualNetworkSeed }) {
51 const { bind: keyBind, code } = useKeyfileGenerator();
55 const { bind: keyBind, code } = useKeyfileGenerator();
52
56
53 return (
57 return (
54− <>
58+ <Window className="os-home">
55− <Grid>
59+ <HeaderPane>
60+ <Row className="header-row">
61+ <h5>OS</h5>
62+ <Button
63+ className="header-button keyfile"
64+ disabled={!hasSetNetworkingKeys}
65+ onClick={keyBind.download}>
66+ <KeyfileIcon />
67+ Download Keyfile
68+ </Button>
56− <Grid.Item full className="mv7 f5">
69+ {/* <Grid.Item
57− Urbit OS
70+ full
58− </Grid.Item>
71+ detail="A keyfile is used to boot your Urbit OS"
72+ as={DownloadKeyfileButton}
73+ {...keyBind}
74+ /> */}
75+ </Row>
76+ </HeaderPane>
77+ <BodyPane>
59 {showSponsor && (
78 {showSponsor && (
60− <>
79+ <Row className="between-row management">
61− <Grid.Divider />
80+ <Box>
62− <Grid.Item
81+ <Row>
63− full
64− as={ForwardButton}
65− detail="A sponsor finds new peers in your network"
66− accessory="Change"
67− onClick={goChangeSponsor}>
68− <span className="mono">{sponsor}</span>
82+ <span className="mono">{sponsor}</span>
69− <span className="f7 bg-black white p1 mb2 ml2 r4">SPONSOR</span>
83+ <span className="f7 bg-black white p1 mb2 ml2 r4">SPONSOR</span>
70− </Grid.Item>
84+ </Row>
71− </>
85+ <Box className="subtitle">
72− )}
86+ Your sponsor finds new peers in your network
73− {hasSetNetworkingKeys && (
87+ </Box>
74− <>
88+ </Box>
75− <Grid.Divider />
89+ <Row>
76− <Grid.Item
90+ <Button className="secondary" onClick={goChangeSponsor}>
77− full
91+ Change
78− detail="A keyfile is used to boot your Urbit OS"
92+ </Button>
79− as={DownloadKeyfileButton}
93+ </Row>
80− {...keyBind}
94+ </Row>
81− />
82− </>
83− )}
84− <AccessCode code={code} />
85− <Grid.Divider />
86− <Grid.Item full as={ForwardButton} onClick={goNetworkingKeys}>
87− {networkKeysAction} Networking Keys
88− </Grid.Item>
89− {hasSetNetworkingKeys && (
90− <>
91− <Grid.Divider />
92− <Grid.Item
93− full
94− as={ForwardButton}
95− accessory={showKeys ? '▲' : '▼'}
96− onClick={toggleShowKeys}>
97− View Networking Keys
98− </Grid.Item>
99− {showKeys && <Grid.Item full as={NetworkingKeys} point={point} />}
100− </>
101 )}
95 )}
96+ <Row className="between-row management">
97+ <Box>
98+ <Box>Network Keys</Box>
99+ <Box className="subtitle">
100+ {hasSetNetworkingKeys
101+ ? `Revision: ${details.keyRevisionNumber}`
102+ : 'Unset'}
103+ </Box>
104+ </Box>
105+ <Row>
106+ <Button className="secondary" onClick={goNetworkingKeys}>
107+ {hasSetNetworkingKeys ? 'Reset' : 'Initialize'}
108+ </Button>
109+ {hasSetNetworkingKeys && (
110+ <Button className="secondary" onClick={toggleShowKeys}>
111+ {showKeys ? 'Hide' : 'View'}
112+ </Button>
113+ )}
114+ </Row>
115+ </Row>
116+
117+ {showKeys && <Grid.Item full as={NetworkingKeys} point={point} />}
118+
102 {!hasSetNetworkingKeys && (
119 {!hasSetNetworkingKeys && (
103 <>
120 <>
104 <Grid.Item full as={AlertBox} className="mt4">
121 <Grid.Item full as={AlertBox} className="mt4">
@@ -106,7 +123,19 @@ export default function UrbitOSHome({ manualNetworkSeed }) {
106 </Grid.Item>
123 </Grid.Item>
107 </>
124 </>
108 )}
125 )}
109− </Grid>
126+
110− </>
127+ {!!code && (
128+ <Row className="between-row management">
129+ <Box>
130+ <Box>Access Key</Box>
131+ <Box className="subtitle">Your passkey to login to Landscape</Box>
132+ </Box>
133+ <Row>
134+ <CopiableWithTooltip text={code} className="copy-button" />
135+ </Row>
136+ </Row>
137+ )}
138+ </BodyPane>
139+ </Window>
111 );
140 );
112}
141}
src/views/UrbitOS/UrbitOS.scssnew file+5
@@ -0,0 +1,5 @@
1+.os-home {
2+ .keyfile > svg {
3+ margin-right: 4px;
4+ }
5+}