@@ -1,348 +1,146 @@
1−import React, { useMemo, useEffect, useCallback, useState } from 'react';
1+import React, { useEffect, useCallback, useState } from 'react';
2−import { Grid, Flex, Button } from 'indigo-react';
2+import { Grid, Button } from 'indigo-react';
3−import * as ob from 'urbit-ob';
4−import { Just, Nothing } from 'folktale/maybe';
5import cn from 'classnames';
3import cn from 'classnames';
4+import * as azimuth from 'azimuth-js';
5+import { Icon, Row, StatelessTextInput } from '@tlon/indigo-react';
7import { usePointCursor } from 'store/pointCursor';
7import { usePointCursor } from 'store/pointCursor';
8import { usePointCache } from 'store/pointCache';
8import { usePointCache } from 'store/pointCache';
9import { useWallet } from 'store/wallet';
9import { useWallet } from 'store/wallet';
10−
10+import { useNetwork } from 'store/network';
11−import View from 'components/View';
11+import { useRollerStore } from 'store/roller';
12−import MaybeSigil from 'components/MaybeSigil';
13−import { matchBlinky } from 'components/Blinky';
15import { useLocalRouter } from 'lib/LocalRouter';
13import { useLocalRouter } from 'lib/LocalRouter';
16−import useInvites from 'lib/useInvites';
17import * as need from 'lib/need';
14import * as need from 'lib/need';
18import * as wg from 'lib/walletgen';
15import * as wg from 'lib/walletgen';
16+import useRoller from 'lib/useRoller';
17+import { convertToInt } from 'lib/convertToInt';
18+import { generateUrl } from 'lib/utils/invite';
20−import { ReactComponent as SearchIcon } from 'assets/search.svg';
20+import { useIssueChild } from 'views/IssueChild';
21−import CopyButton from 'components/copiable/CopyButton';
21+import View from 'components/View';
22−import { useNetwork } from 'store/network';
23import L2BackHeader from 'components/L2/Headers/L2BackHeader';
22import L2BackHeader from 'components/L2/Headers/L2BackHeader';
24import Window from 'components/L2/Window/Window';
23import Window from 'components/L2/Window/Window';
25import HeaderPane from 'components/L2/Window/HeaderPane';
24import HeaderPane from 'components/L2/Window/HeaderPane';
26−import { Icon, Row, StatelessTextInput } from '@tlon/indigo-react';
27import BodyPane from 'components/L2/Window/BodyPane';
25import BodyPane from 'components/L2/Window/BodyPane';
28−import { useRollerStore } from 'store/roller';
29−import useRoller from 'lib/useRoller';
30−import FeeDropdown from 'components/L2/Dropdowns/FeeDropdown';
31import LoadingOverlay from 'components/L2/LoadingOverlay';
26import LoadingOverlay from 'components/L2/LoadingOverlay';
32−import { generateUrl } from 'lib/utils/invite';
33import CopiableWithTooltip from 'components/copiable/CopiableWithTooltip';
27import CopiableWithTooltip from 'components/copiable/CopiableWithTooltip';
34import Paginator from 'components/L2/Paginator';
28import Paginator from 'components/L2/Paginator';
29+import InlineEthereumTransaction from 'components/InlineEthereumTransaction';
36import './Cohort.scss';
31import './Cohort.scss';
37−import useInviter from 'lib/useInviter';
32+import { getStoredInvites, setStoredInvites } from 'store/storage/roller';
39const INVITES_PER_PAGE = 7;
34const INVITES_PER_PAGE = 7;
40const DEFAULT_NUM_INVITES = 5;
35const DEFAULT_NUM_INVITES = 5;
42−function SearchInput({ className, value, onChange }) {
37+export default function InviteCohort() {
43− return (
38+ const { pop, push, names } = useLocalRouter();
44− <Flex align="center" full className={cn('b b-gray2 b1', className)}>
39+ const { authToken } = useWallet();
45− <Flex.Item className="p2">
40+ const {
46− <SearchIcon />
41+ nextRoll,
47− </Flex.Item>
42+ currentL2,
48−
43+ pendingTransactions,
49− <Flex.Item
44+ invites,
50− flex={1}
45+ } = useRollerStore();
51− as={'input'}
46+ const { generateInviteCodes, getPendingTransactions } = useRoller();
52− value={value}
47+ const { pointCursor } = usePointCursor();
53− onChange={onChange}
48+ const { contracts } = useNetwork();
54− className="b-none h7"
49+ const { getDetails } = usePointCache();
60−function CohortMember({ point, pending = false, className, ...rest }) {
51+ const point = need.point(pointCursor);
61− const { sentInvites } = useInvites(point);
52+ const _contracts = need.contracts(contracts);
62− const patp = useMemo(() => ob.patp(point), [point]);
63− const colors = pending ? ['#ee892b', '#FFFFFF'] : ['#000000', '#FFFFFF'];
65− const DetailText = useCallback(
54+ const pointSize = azimuth.azimuth.getPointSize(point);
66− () =>
55+ const isParent = pointSize !== azimuth.azimuth.PointSize.Planet;
67− pending ? 'Pending' : <> {matchBlinky(sentInvites)} points invited </>,
56+ const details = need.details(getDetails(point));
68− [pending, sentInvites]
57+ const networkRevision = convertToInt(details.keyRevisionNumber, 10);
69− );
58+ const networkKeysNotSet = !currentL2 && isParent && networkRevision === 0;
71− return (
60+ const goUrbitOS = useCallback(() => push(names.URBIT_OS), [push, names]);
72− <Flex justify="between" className={cn('b1 b-gray2', className)} {...rest}>
73− <Flex.Item className="w9 h9">
74− <MaybeSigil patp={Just(patp)} size={64} colors={colors} />
77− <Flex.Item
62+ const [numInvites, setNumInvites] = useState(
78− flex={1}
63+ currentL2 ? DEFAULT_NUM_INVITES : 1
82− className="mono f6 ph4">
83− <Flex.Item>{patp}</Flex.Item>
84− <Flex.Item className="gray4">
90−}
65+ const [showInviteForm, setShowInviteForm] = useState(false);
91−function CohortList({
66+ const [loading, setLoading] = useState(false);
92− acceptedPoints,
67+ const [error, setError] = useState(null);
93− pendingPoints,
68+ const [l1Invite, setL1Invite] = useState(null);
98− const { syncInvites } = usePointCache();
100− const [query, setQuery] = useState('');
70+ const { construct, unconstruct, completed, bind } = useIssueChild();
72+ // Set up the invite spawn if on L1
102 useEffect(() => {
73 useEffect(() => {
103− acceptedPoints.map(syncInvites);
74+ const _authToken = authToken.getOrElse(null);
104− }, [acceptedPoints, syncInvites]);
106− const handleChange = useCallback(
108− setQuery(e.target.value);
114− const filterPoints = useCallback(
116− points.filter(p => {
117− const patp = ob.patp(p).slice(1);
118− return patp.startsWith(query);
122− const selectInvite = useCallback(
124− onSelectInvite(point);
128− const _pendingPoints = filterPoints(pendingPoints);
129− const _acceptedPoints = filterPoints(acceptedPoints);
131− const offset = onlyPending ? 0 : _acceptedPoints.length;
134− <Grid gap={3} className={cn('mt4', className)}>
135− <Grid.Item full as={SearchInput} value={query} onChange={handleChange} />
138− _acceptedPoints.map((p, idx) => (
141− half={(idx % 2) + 1}
144− onClick={selectInvite(p)}
147− {_pendingPoints.map((p, idx) => (
150− half={((offset + idx) % 2) + 1}
154− onClick={selectInvite(p)}
159− {_pendingPoints.length === 0 &&
160− (_acceptedPoints.length === 0 || onlyPending) && (
161− <Grid.Item full className="p4 t-center">
163− No invites accepted yet.
170−function hideIf(hider, hidden) {
171− return hider ? '●●●●●●●' : hidden;
174−function CohortMemberExpanded({ point, className, ...rest }) {
76+ const setUpInvite = async () => {
175− const { sentInvites, acceptedInvites } = useInvites(point);
77+ setLoading(true);
176− const { getDetails } = usePointCache();
78+ const [invitePoint] = await azimuth.azimuth.getUnspawnedChildren(
177− const { authToken } = useWallet();
79+ _contracts,
178− const { contracts } = useNetwork();
80+ point
179− const details = getDetails(point);
81+ );
180− const { active } = details.getOrElse({});
181− const patp = useMemo(() => ob.patp(point), [point]);
182− const colors = useMemo(
183− () => (!active ? ['#ee892b', '#FFFFFF'] : ['#000000', '#FFFFFF']),
187− const [code, setCode] = useState(Nothing());
188− const [codeVisible, setCodeVisible] = useState(false);
190− useEffect(() => {
83+ // TODO: figure out how to store the ticket and invite info
191− const fetchWallet = async () => {
192− const _authToken = authToken.getOrElse(null);
193− const _details = details.getOrElse(null);
194− const _contracts = contracts.getOrElse(null);
205 const { ticket, owner } = await wg.generateTemporaryDeterministicWallet(
84 const { ticket, owner } = await wg.generateTemporaryDeterministicWallet(
207 _authToken
86 _authToken
210− if (owner.keys.address !== _details.transferProxy) {
89+ construct(invitePoint, owner.keys.address);
211− setCode(Just(null));
90+ setL1Invite({ ticket, planet: invitePoint });
212− } else {
91+ setLoading(false);
213− setCode(Just(ticket));
217− }, [authToken, codeVisible, contracts, details, point]);
220− setCodeVisible(false);
224− <Grid justify="between" className={cn('b1 b-gray2', className)} {...rest}>
225− <Grid.Item rows={[1, 4]} cols={[1, 4]} className="h10 w10">
226− <MaybeSigil patp={Just(patp)} size={64} colors={colors} />
229− <Grid.Item cols={[4, 13]} className="mono mt-auto ">
237− className="mt-auto mb1 f6 gray4"
242− <Grid.Item cols={[4, 10]} className="mb-auto mt1 f6 gray4">
243− {(code.getOrElse(true) === null &&
244− 'This invite code cannot be recovered') ||
245− hideIf(!codeVisible, matchBlinky(code))}
250− className="mh-auto f6 underline pointer"
251− onClick={() => setCodeVisible(true)}>
255− {codeVisible && code.getOrElse('') !== null && (
257− className="mh-auto f6 t-right"
260− text={code.getOrElse('')}
270− className="mt-auto mb1 f6 gray4"
273− {matchBlinky(sentInvites)} invites sent
277− className="mb-auto mt1 f6 gray4"
280− {matchBlinky(acceptedInvites)} invites accepted
288−export default function InviteCohort() {
94+ if (!currentL2 && _contracts && point && _authToken && showInviteForm) {
289− const { pop } = useLocalRouter();
95+ setUpInvite();
291− nextRoll,
97+ unconstruct();
292 currentL2,
100 currentL2,
293− pendingTransactions,
101+ point,
294− invites,
102+ _contracts,
295− } = useRollerStore();
103+ authToken,
296− const { generateInvites } = useInviter();
104+ construct,
297− const { generateInviteCodes, getPendingTransactions } = useRoller();
105+ showInviteForm,
299− const { pointCursor } = usePointCursor();
107+ ]);
300− const point = need.point(pointCursor);
302− const [page, setPage] = useState(0);
109+ const l1Complete = useCallback(() => {
110+ const { available, pending, claimed } = getStoredInvites(point);
111+ setStoredInvites(point, {
112+ available: [...available, l1Invite],
116+ setShowInviteForm(false);
117+ }, [l1Invite, point]);
304− const { pendingPoints } = useInvites(point);
119+ useEffect(() => (completed ? l1Complete() : null), [completed, l1Complete]);
306− const displayedInvites = currentL2 ? invites : pendingPoints.getOrElse([]);
121+ const [page, setPage] = useState(0);
308− const invitesToDisplay = displayedInvites.slice(
123+ const invitesToDisplay = invites.slice(
309 page * INVITES_PER_PAGE,
124 page * INVITES_PER_PAGE,
310 (page + 1) * INVITES_PER_PAGE
125 (page + 1) * INVITES_PER_PAGE
313 const hasPending = Boolean(pendingTransactions.length);
128 const hasPending = Boolean(pendingTransactions.length);
314− const hasInvites = Boolean(displayedInvites.length);
129+ const hasInvites = Boolean(invites.length);
316− const [numInvites, setNumInvites] = useState(DEFAULT_NUM_INVITES);
317− const [showInviteForm, setShowInviteForm] = useState(false);
318− const [loading, setLoading] = useState(false);
319− const [error, setError] = useState(null);
321 const createInvites = useCallback(async () => {
131 const createInvites = useCallback(async () => {
322 setLoading(true);
132 setLoading(true);
325− const invites = await generateInviteCodes(numInvites);
134+ const invites = await generateInviteCodes(numInvites);
326− console.log('INVITES', invites);
135+ console.log('L2 INVITES', invites);
327− getPendingTransactions(point);
136+ getPendingTransactions(point);
329− const invites = await generateInvites(numInvites);
330− console.log('INVITES', invites);
332− setShowInviteForm(false);
333 } catch (error) {
138 } catch (error) {
334 setError(error);
139 setError(error);
335 } finally {
140 } finally {
336 setLoading(false);
141 setLoading(false);
339− generateInviteCodes,
341− getPendingTransactions,
143+ }, [generateInviteCodes, numInvites, getPendingTransactions, point, pop]);
347 const getContent = () => {
145 const getContent = () => {
348 if (hasInvites) {
146 if (hasInvites) {
@@ -378,6 +176,18 @@ export default function InviteCohort() {
378 Check back in <span className="timer"> {nextRoll}</span>
176 Check back in <span className="timer"> {nextRoll}</span>
179+ } else if (networkKeysNotSet) {
182+ This star's network keys are not set.
184+ Either set the network keys or{' '}
185+ <span className="migrate" onClick={() => null}>
186+ {/* TODO: change this to navigate to migration */}
187+ migrate this star to L2.
@@ -398,8 +208,8 @@ export default function InviteCohort() {
398 const header = (
208 const header = (
401− <span className="number-emphasis"> {displayedInvites.length} </span>
211+ <span className="number-emphasis"> {invites.length} </span>
402− Planet Code{displayedInvites.length === 1 ? '' : 's'}
212+ Planet Code{invites.length === 1 ? '' : 's'}
@@ -415,18 +225,29 @@ export default function InviteCohort() {
415 </HeaderPane>
225 </HeaderPane>
416 <BodyPane>
226 <BodyPane>
417 <div className="upper">
227 <div className="upper">
418− <Row className="points-input">
229+ <Row className="points-input">
419− I want to generate
230+ I want to generate
420− <StatelessTextInput
231+ <StatelessTextInput
421− className="input-box"
232+ className="input-box"
422− value={numInvites}
233+ value={numInvites}
423− maxLength="3"
234+ maxLength="3"
424− onChange={e =>
235+ onChange={e =>
425− setNumInvites(Number(e.target.value.replace(/\D/g, '')))
236+ setNumInvites(Number(e.target.value.replace(/\D/g, '')))
428− planet invite code{numInvites > 1 ? 's' : ''}
239+ planet invite code{numInvites > 1 ? 's' : ''}
242+ <div className="migration-prompt">
243+ ETH fees are very high currently. You can spawn invites for
245+ <span className="migrate" onClick={() => null}>
246+ {/* TODO: change this to navigate to migration */}
247+ migrating this star to L2.
430 {/* <div>Destination</div>
251 {/* <div>Destination</div>
431 <div className="ship-or-address">Ship or ethereum address</div>
252 <div className="ship-or-address">Ship or ethereum address</div>
432 <Dropdown value={dropdownValue}>
253 <Dropdown value={dropdownValue}>
@@ -439,22 +260,30 @@ export default function InviteCohort() {
440 {/* <Inviter /> */}
261 {/* <Inviter /> */}
441 <div className="lower">
262 <div className="lower">
442− <Row className="next-roll">
264+ <Row className="next-roll">
443− <span>{currentL2 ? 'Next Roll in' : 'Transaction Fee'}</span>
265+ <span>Next Roll in</span>
445 <span className="timer">{nextRoll}</span>
266 <span className="timer">{nextRoll}</span>
451− as={'button'}
271+ as={'button'}
452− className="generate-codes"
272+ className="generate-codes"
455− onClick={createInvites}>
275+ onClick={createInvites}>
456− Generate Planet Codes ({numInvites})
276+ Generate Planet Code{currentL2 ? `s (${numInvites})` : ''}
457− </Button>
277+ </Button>
281+ as={InlineEthereumTransaction}
282+ label="Generate Planet Code"
284+ onReturn={() => pop()}
459 </BodyPane>
288 </BodyPane>
460 </Window>
289 </Window>
@@ -463,8 +292,15 @@ export default function InviteCohort() {
295+ const showGenerateButton = !hasPending && !hasInvites && !networkKeysNotSet;
467− <View pop={pop} inset className="cohort" hideBack header={<L2BackHeader />}>
301+ className="cohort"
303+ header={<L2BackHeader hideBalance />}>
469 <HeaderPane>
305 <HeaderPane>
470 {!hasInvites ? (
306 {!hasInvites ? (
@@ -483,7 +319,7 @@ export default function InviteCohort() {
483 <div className={`content ${!hasInvites ? 'center' : ''}`}>
319 <div className={`content ${!hasInvites ? 'center' : ''}`}>
484 {getContent()}
320 {getContent()}
486− {!hasPending && !hasInvites && (
322+ {showGenerateButton && (
488 as={'button'}
324 as={'button'}
489 className="generate-button"
325 className="generate-button"
@@ -493,11 +329,21 @@ export default function InviteCohort() {
493 Generate Codes
329 Generate Codes
494 </Button>
330 </Button>
332+ {networkKeysNotSet && (
335+ className="generate-button"
338+ onClick={goUrbitOS}>
496 {hasInvites && (
342 {hasInvites && (
497 <Paginator
343 <Paginator
498 page={page}
344 page={page}
499 numPerPage={INVITES_PER_PAGE}
345 numPerPage={INVITES_PER_PAGE}
500− numElements={displayedInvites.length}
346+ numElements={invites.length}
501 goPrevious={() => setPage(page - 1)}
347 goPrevious={() => setPage(page - 1)}
502 goNext={() => setPage(page + 1)}
348 goNext={() => setPage(page + 1)}