on master help sign in
add invite group detail view
commit44dc2fe2dbd81dac56a01d8c3718b5d1f8142530 authorLiam Fitzgerald <liam@urizen.co> dateWed 1 Jan 2020, 17:29 (7 years ago) parent4db9a8e
10 files changed+770−44
src/assets/search.svg+3 src/components/BarGraph.js+29 src/components/Chip.js+18 src/components/InviteSigilList.js+47 src/indigo-react/indigo.css+4 src/lib/routeNames.js+1 src/lib/router.js+2 src/views/Invite/Cohort.js+479 src/views/Party/ShowParty.js+159 src/views/Point.js+28−44
src/assets/search.svgnew file+3
@@ -0,0 +1,3 @@
1+<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
2+<path d="M9.18806 10.1969C8.04395 11.0861 6.60411 11.5056 5.16172 11.37C3.71932 11.2344 2.38282 10.5538 1.42436 9.46684C0.465895 8.3799 -0.0424681 6.96833 0.00278089 5.51955C0.0480299 4.07077 0.643489 2.69371 1.66791 1.66876C2.69234 0.643816 4.0687 0.0480543 5.51674 0.00278231C6.96479 -0.0424897 8.37564 0.466132 9.46203 1.42508C10.5484 2.38404 11.2286 3.72121 11.3642 5.16434C11.4998 6.60747 11.0805 8.04805 10.1917 9.19274L14 12.9887L12.9892 14L9.19518 10.1969H9.18806ZM5.70012 9.96902C6.26099 9.96902 6.81637 9.85849 7.33454 9.64375C7.85272 9.429 8.32354 9.11425 8.72014 8.71745C9.11673 8.32066 9.43133 7.84959 9.64596 7.33115C9.8606 6.81271 9.97107 6.25705 9.97107 5.6959C9.97107 5.13474 9.8606 4.57908 9.64596 4.06064C9.43133 3.54221 9.11673 3.07114 8.72014 2.67434C8.32354 2.27755 7.85272 1.96279 7.33454 1.74805C6.81637 1.5333 6.26099 1.42278 5.70012 1.42278C4.56739 1.42278 3.48106 1.87298 2.6801 2.67434C1.87914 3.47571 1.42917 4.5626 1.42917 5.6959C1.42917 6.8292 1.87914 7.91609 2.6801 8.71745C3.48106 9.51882 4.56739 9.96902 5.70012 9.96902Z" fill="#7F7F7F"/>
3+</svg>
src/components/BarGraph.jsnew file+29
@@ -0,0 +1,29 @@
1+import React from 'react';
2+import cn from 'classnames';
3+import { Nothing } from 'folktale/maybe';
4+
5+const BarGraph = ({
6+ className,
7+ available = Nothing(),
8+ sent = Nothing(),
9+ accepted = Nothing(),
10+}) => {
11+ const _available = available.getOrElse(1);
12+ const _sent = sent.getOrElse(0);
13+ const _accepted = accepted.getOrElse(0);
14+ const total = _available + _sent + _accepted;
15+ return (
16+ <div className={cn('h7 p1 b1 b-black flex', className)}>
17+ <div
18+ className="bg-black"
19+ style={{ width: `${(_accepted / total) * 100}%` }}></div>
20+ <div
21+ className="bg-yellow4"
22+ style={{ width: `${((_sent - _accepted) / total) * 100}%` }}></div>
23+
24+ <div className="flex-grow" />
25+ </div>
26+ );
27+};
28+
29+export default BarGraph;
src/components/Chip.jsnew file+18
@@ -0,0 +1,18 @@
1+import React from 'react';
2+import cn from 'classnames';
3+
4+export default function Chip({ children, color, className }) {
5+ const bgColor = `bg-${color}1`;
6+ const fgColor = `${color}4`;
7+ return (
8+ <div
9+ className={cn(
10+ className,
11+ 'h6 r-full flex-center ph2 mh2',
12+ bgColor,
13+ fgColor
14+ )}>
15+ {children}
16+ </div>
17+ );
18+}
src/components/InviteSigilList.jsnew file+47
@@ -0,0 +1,47 @@
1+import React from 'react';
2+import cn from 'classnames';
3+import { Just, Nothing } from 'folktale/maybe';
4+import * as ob from 'urbit-ob';
5+import { take } from 'lodash';
6+
7+import MaybeSigil from 'components/MaybeSigil';
8+
9+const InviteSigilList = ({ className, pendingPoints, acceptedPoints }) => {
10+ const _acceptedPoints = take(
11+ acceptedPoints.getOrElse([]).map(x => Just(ob.patp(x))),
12+ 6
13+ );
14+
15+ const _pendingPoints = take(
16+ pendingPoints.getOrElse([]).map(x => Just(ob.patp(x))),
17+ 6 - _acceptedPoints.length
18+ );
19+
20+ const empty = [
21+ ...Array(
22+ Math.max(6 - _acceptedPoints.length - _pendingPoints.length, 0)
23+ ).keys(),
24+ ].map(() => Nothing());
25+
26+ const renderSigil = (points, colors, klassName) => {
27+ return (
28+ <>
29+ {points.map((point, idx) => (
30+ <div className={cn(klassName, 'h9 w9')}>
31+ <MaybeSigil patp={point} size={50} colors={colors} />
32+ </div>
33+ ))}
34+ </>
35+ );
36+ };
37+
38+ return (
39+ <div className={cn('flex justify-between', className)}>
40+ {renderSigil(_acceptedPoints, ['#000000', '#FFFFFF'])}
41+ {renderSigil(_pendingPoints, ['#ee892b', '#FFFFFF'])}
42+ {renderSigil(empty, [], 'b1 b-black')}
43+ </div>
44+ );
45+};
46+
47+export default InviteSigilList;
src/indigo-react/indigo.css+4
@@ -1573,6 +1573,10 @@ caption,
15731573.justify-around {
15741574 justify-content: space-around; }
15751575
1576+.justify-evenly {
1577+ justify-content: space-evenly;
1578+}
1579+
15761580.flex-center {
15771581 display: flex;
15781582 justify-content: center;
src/lib/routeNames.js+1
@@ -20,4 +20,5 @@ export const ROUTE_NAMES = {
2020 CREATE_GALAXY: Symbol('CREATE_GALAXY'),
2121 ISSUE_CHILD: Symbol('ISSUE_CHILD'),
2222 PARTY_SET_POOL_SIZE: Symbol('PARTY_SET_POOL_SIZE'),
23+ PARTY: Symbol('PARTY'),
2324};
src/lib/router.js+2
@@ -10,6 +10,7 @@ import Invite from 'views/Invite';
1010import Activate from 'views/Activate';
1111
1212import PartySetPoolSize from 'views/Party/PartySetPoolSize';
13+import ShowParty from 'views/Party/ShowParty';
1314import AcceptTransfer from 'views/AcceptTransfer';
1415import CancelTransfer from 'views/CancelTransfer';
1516
@@ -30,4 +31,5 @@ export const ROUTES = {
3031 [ROUTE_NAMES.ACCEPT_TRANSFER]: AcceptTransfer,
3132 // TODO: replace this with deep link to AdminCancelTransfer
3233 [ROUTE_NAMES.CANCEL_TRANSFER]: CancelTransfer,
34+ [ROUTE_NAMES.PARTY]: ShowParty,
3335};
src/views/Invite/Cohort.jsnew file+479
@@ -0,0 +1,479 @@
1+import React, { useMemo, useCallback, useState, useEffect } from 'react';
2+import { Grid, ErrorText } from 'indigo-react';
3+import { get } from 'lodash';
4+import * as azimuth from 'azimuth-js';
5+import { FORM_ERROR } from 'final-form';
6+import { toWei, toBN } from 'web3-utils';
7+
8+import * as need from 'lib/need';
9+import * as wg from 'lib/walletgen';
10+import * as tank from 'lib/tank';
11+// import useMailer from 'lib/useMailer';
12+import timeout from 'lib/timeout';
13+import useCopiable from 'lib/useCopiable';
14+
15+import Tabs from 'components/Tabs';
16+import ProgressButton from 'components/ProgressButton';
17+import { ForwardButton } from 'components/Buttons';
18+import NeedFundsNotice from 'components/NeedFundsNotice';
19+
20+import { useNetwork } from 'store/network';
21+import { useWallet } from 'store/wallet';
22+import { usePointCursor } from 'store/pointCursor';
23+import { usePointCache } from 'store/pointCache';
24+
25+// import { } from ''
26+import {
27+ signTransaction,
28+ sendSignedTransaction,
29+ waitForTransactionConfirm,
30+ hexify,
31+} from 'lib/txn';
32+import { MIN_PLANET, GAS_LIMITS } from 'lib/constants';
33+import pluralize from 'lib/pluralize';
34+import useGasPrice from 'lib/useGasPrice';
35+import useSetState from 'lib/useSetState';
36+
37+import EmailChipInput from 'form/EmailChipInput';
38+import FormError from 'form/FormError';
39+import SubmitButton from 'form/SubmitButton';
40+import BridgeForm from 'form/BridgeForm';
41+import {
42+ buildEmailArrayValidator,
43+ composeValidator,
44+ hasErrors,
45+} from 'form/validators';
46+import { WARNING, onlyHasWarning } from 'form/helpers';
47+
48+const GAS_LIMIT = GAS_LIMITS.GIFT_PLANET;
49+
50+const STATUS = {
51+ INPUT: 'INPUT',
52+ SENDING: 'SENDING',
53+ SUCCESS: 'SUCCESS',
54+ FAILURE: 'FAILURE',
55+};
56+
57+const nameForEmailField = i => `emails[${i}]`;
58+
59+const useInviter = sendInvites => {
60+ const { contracts, web3, networkType } = useNetwork();
61+ const { wallet, walletType, walletHdPath } = useWallet();
62+ const { pointCursor } = usePointCursor();
63+ const { syncInvites } = usePointCache();
64+ const { gasPrice } = useGasPrice();
65+ const point = need.point(pointCursor);
66+
67+ const [needFunds, setNeedFunds] = useState(null);
68+ const [invites, setInvites] = useState([]);
69+
70+ const [progress, setProgress] = useState(0);
71+ const [txStatus, setTxStatus] = useState(STATUS.INPUT);
72+ const [errors, addError] = useSetState();
73+
74+ const isDone = txStatus === STATUS.SUCCESS;
75+ useEffect(() => {
76+ if (isDone) {
77+ syncInvites(point);
78+ }
79+ }, [isDone, syncInvites, point]);
80+
81+ const resetInvites = useCallback(() => {
82+ setProgress(0);
83+ setInvites([]);
84+ setTxStatus(STATUS.INPUT);
85+ }, [setProgress, setInvites, setTxStatus]);
86+ const generateInvites = useCallback(
87+ async numInvites => {
88+ const _contracts = contracts.getOrElse(null);
89+ const _web3 = web3.getOrElse(null);
90+ const _wallet = wallet.getOrElse(null);
91+ if (!_contracts || !_web3 || !_wallet) {
92+ // not using need because we want a custom error
93+ throw new Error('Internal Error: Missing Contracts/Web3/Wallet');
94+ }
95+
96+ const nonce = await _web3.eth.getTransactionCount(_wallet.address);
97+ const chainId = await _web3.eth.net.getId();
98+ debugger;
99+ const planets = await azimuth.delegatedSending.getPlanetsToSend(
100+ _contracts,
101+ point,
102+ numInvites
103+ );
104+
105+ setTxStatus(STATUS.SENDING);
106+ // account for the race condition where invites got used up while we were
107+ // composing our target list
108+ console.log('a');
109+ if (planets.length < numInvites) {
110+ // resync invites to the cache, since they're out of date
111+ syncInvites(point);
112+
113+ setTxStatus(STATUS.FAILURE);
114+
115+ return {
116+ [FORM_ERROR]:
117+ `Can currently only send ${planets.length} invites. ` +
118+ `Please remove invites until you are within the limit.`,
119+ };
120+ }
121+
122+ console.log('b');
123+ // NB(shrugs) - must be processed in serial because main thread, etc
124+ let signedInvites = [];
125+ let errorCount = 0;
126+ for (let i = 0; i < numInvites; i++) {
127+ console.log(`c: ${i}`);
128+ const name = nameForEmailField(i);
129+ setProgress(x => x + 1);
130+ try {
131+ const planet = planets[i];
132+
133+ const { ticket, owner } = await wg.generateTemporaryTicketAndWallet(
134+ MIN_PLANET
135+ // we're always giving planets, so generate a ticket of the correct size
136+ );
137+
138+ const inviteTx = azimuth.delegatedSending.sendPoint(
139+ _contracts,
140+ point,
141+ planet,
142+ owner.keys.address
143+ );
144+
145+ const signedTx = await signTransaction({
146+ wallet: _wallet,
147+ walletType,
148+ walletHdPath,
149+ networkType,
150+ chainId,
151+ nonce: nonce + i,
152+ // TODO: ^ make a useTransactionSigner to encapsulate this logic
153+ txn: inviteTx,
154+ gasPrice: gasPrice.toFixed(), // expects string gwei
155+ gasLimit: GAS_LIMIT.toFixed(),
156+ });
157+
158+ const rawTx = hexify(signedTx.serialize());
159+
160+ signedInvites.push({ ticket, signedTx, rawTx });
161+ } catch (error) {
162+ console.error(error);
163+ return {
164+ [WARNING]: `There was an error while generating wallets.`,
165+ };
166+ }
167+ }
168+
169+ const totalCost = toBN(GAS_LIMIT)
170+ .mul(toBN(gasPrice))
171+ .mul(toBN(numInvites));
172+ const tankWasUsed = await tank.ensureFundsFor(
173+ _web3,
174+ point,
175+ _wallet.address,
176+ toWei(totalCost.toString(), 'gwei'),
177+ Object.keys(invites).map(name => invites[name].rawTx),
178+ (address, minBalance, balance) =>
179+ setNeedFunds({ address, minBalance, balance }),
180+ () => setNeedFunds(undefined)
181+ );
182+
183+ let unsentInvites = [];
184+ let confirmedInvites = [];
185+ const txAndMailings = signedInvites.map(async invite => {
186+ try {
187+ const txHash = await sendSignedTransaction(
188+ _web3,
189+ invite.signedTx,
190+ tankWasUsed
191+ );
192+
193+ await waitForTransactionConfirm(_web3, txHash);
194+ confirmedInvites.push(invite);
195+ } catch (error) {
196+ console.error(error);
197+ unsentInvites.push(invite);
198+ return;
199+ }
200+ });
201+
202+ await Promise.all(txAndMailings);
203+
204+ setTxStatus(STATUS.SUCCESS);
205+ setInvites(confirmedInvites);
206+ console.log(unsentInvites);
207+
208+ if (unsentInvites.length > 0) {
209+ return { [FORM_ERROR]: unsentInvites };
210+ }
211+
212+ if (errorCount > 0) {
213+ return {
214+ [WARNING]: `There ${pluralize(errorCount, 'was', 'were')} ${pluralize(
215+ errorCount,
216+ 'error'
217+ )} while generating wallets. You can still send the invites that generated correctly.`,
218+ };
219+ }
220+ },
221+ [
222+ contracts,
223+ gasPrice,
224+ invites,
225+ networkType,
226+ point,
227+ syncInvites,
228+ wallet,
229+ walletHdPath,
230+ walletType,
231+ web3,
232+ ]
233+ );
234+
235+ return {
236+ progress,
237+ txStatus,
238+ needFunds,
239+ invites,
240+ generateInvites,
241+ resetInvites,
242+ };
243+};
244+
245+const InviteMail = () => {
246+ const {
247+ progress,
248+ txStatus,
249+ needFunds,
250+ invites,
251+ generateInvites,
252+ resetInvites,
253+ } = useInviter();
254+
255+ const [status, setStatus] = useState(STATUS.INPUT);
256+
257+ const [count, setCount] = useState(0);
258+ const canInput = status === STATUS.INPUT;
259+ const isSending = txStatus === STATUS.SENDING || status === STATUS.SENDING;
260+ const isFailed = status === STATUS.FAILURE || txStatus === STATUS.FAILURE;
261+ const isDone = status === STATUS.SUCCESS;
262+ const onSubmit = useCallback(
263+ async values => {
264+ console.log('GENERATING');
265+ const emailCount = values.emails.length;
266+ console.log(values);
267+ setCount(emailCount);
268+ setStatus(STATUS.SENDING);
269+ const errors = await generateInvites(emailCount);
270+ console.log(errors);
271+ if (errors) {
272+ setStatus(STATUS.FAILED);
273+ return errors;
274+ }
275+ },
276+ [generateInvites, setCount, setStatus]
277+ );
278+
279+ useEffect(() => {
280+ async function processInvites() {
281+ if (invites.length !== 0 && txStatus === STATUS.SUCCESS) {
282+ console.log(JSON.stringify(invites));
283+ console.log('a');
284+ await timeout(500);
285+
286+ resetInvites();
287+ setStatus(STATUS.SUCCESS);
288+ }
289+ }
290+ processInvites();
291+ }, [invites, setStatus, status, txStatus, resetInvites]);
292+
293+ const buttonText = () => {
294+ switch (status) {
295+ case STATUS.INPUT:
296+ return 'Add to Invite Group';
297+ case STATUS.SENDING:
298+ return `Sending ${pluralize(count, 'Invite')}...`;
299+ case STATUS.SUCCESS:
300+ return `✓ ${pluralize(count, 'Invite')} sent`;
301+ case STATUS.FAILURE:
302+ default:
303+ return 'Error';
304+ }
305+ };
306+
307+ const renderButton = handleSubmit => {
308+ if (canInput) {
309+ return (
310+ <Grid.Item full as={SubmitButton} handleSubmit={handleSubmit}>
311+ {buttonText()}
312+ </Grid.Item>
313+ );
314+ }
315+
316+ if (isSending) {
317+ const percentProgress = progress / count;
318+ return (
319+ <Grid.Item
320+ full
321+ as={ProgressButton}
322+ className="mt4"
323+ solid
324+ disabled
325+ success
326+ progress={percentProgress}>
327+ {buttonText(count)}
328+ </Grid.Item>
329+ );
330+ }
331+ if (isDone) {
332+ return (
333+ <Grid.Item full className="mt4 p3 bg-green3 white f5">
334+ {buttonText(count)}
335+ </Grid.Item>
336+ );
337+ }
338+
339+ return null;
340+ };
341+
342+ const validate = useMemo(
343+ () => composeValidator({ emails: buildEmailArrayValidator() }),
344+ []
345+ );
346+ return (
347+ <Grid.Item full as={Grid}>
348+ <BridgeForm
349+ validate={validate}
350+ initialValues={{ emails: [] }}
351+ onSubmit={onSubmit}>
352+ {({ handleSubmit, values }) => (
353+ <>
354+ <Grid.Item full as={EmailChipInput} name="emails" className="mt4" />
355+ {renderButton(handleSubmit)}
356+
357+ <Grid.Item full as={FormError} />
358+ {needFunds && (
359+ <Grid.Item full as={NeedFundsNotice} {...needFunds} />
360+ )}
361+ </>
362+ )}
363+ </BridgeForm>
364+ </Grid.Item>
365+ );
366+};
367+
368+const InviteUrl = () => {
369+ const {
370+ txStatus,
371+ errors,
372+ needFunds,
373+ invites,
374+ generateInvites,
375+ resetInvites,
376+ } = useInviter();
377+
378+ const [error, setError] = useState();
379+
380+ const generateInvite = useCallback(async () => {
381+ const errors = await generateInvites(1);
382+ if (hasErrors(errors)) {
383+ setError(errors);
384+ }
385+ }, [generateInvites]);
386+
387+ const renderGenerateButton = () => {
388+ if (error) {
389+ return (
390+ <Grid.Item full className="mt4 p3 bg-red3 white f5">
391+ Error generating invite
392+ </Grid.Item>
393+ );
394+ }
395+ switch (txStatus) {
396+ case STATUS.INPUT:
397+ return (
398+ <Grid.Item
399+ solid
400+ full
401+ className="mt4"
402+ as={ForwardButton}
403+ onClick={() => generateInvite()}>
404+ Generate Invite URL
405+ </Grid.Item>
406+ );
407+ case STATUS.SENDING:
408+ return (
409+ <Grid.Item
410+ full
411+ as={ProgressButton}
412+ className="mt4"
413+ solid
414+ disabled
415+ success
416+ progress={0.5}>
417+ Generating Invite URL
418+ </Grid.Item>
419+ );
420+ }
421+
422+ return null;
423+ };
424+
425+ const url = `https://bridge.urbit.org/#${get(invites, '[0].ticket', '')}`;
426+
427+ const [doCopy, didCopy] = useCopiable(url);
428+
429+ return (
430+ <Grid.Item full as={Grid}>
431+ {invites.length > 0 && (
432+ <Grid.Item full className="b-gray3 b1 mv4 p1 flex flex-center">
433+ <div className="flex1 ml1">{url}</div>
434+ <Grid.Item
435+ className="pv3 ph4 bg-black white"
436+ onClick={() => doCopy()}>
437+ {didCopy ? 'Copied' : 'Copy'}
438+ </Grid.Item>
439+ </Grid.Item>
440+ )}
441+ {invites.length === 0 && renderGenerateButton()}
442+ {needFunds && <Grid.Item full as={NeedFundsNotice} {...needFunds} />}
443+ {error && <Grid.Item as={ErrorText}>{JSON.stringify(error)}</Grid.Item>}
444+ </Grid.Item>
445+ );
446+};
447+
448+const InviteCohort = () => {
449+ const [tab, setTab] = useState(NAMES.MAIL);
450+ return (
451+ <Grid.Item
452+ full
453+ as={Tabs}
454+ tabClassName="t-center flex1"
455+ className="flex1"
456+ views={VIEWS}
457+ options={OPTIONS}
458+ currentTab={tab}
459+ onTabChange={setTab}
460+ />
461+ );
462+};
463+
464+const NAMES = {
465+ MAIL: 'MAIL',
466+ URL: 'URL',
467+};
468+
469+const VIEWS = {
470+ [NAMES.MAIL]: InviteMail,
471+ [NAMES.URL]: InviteUrl,
472+};
473+
474+const OPTIONS = [
475+ { text: 'Email', value: NAMES.MAIL },
476+ { text: 'URL', value: NAMES.URL },
477+];
478+
479+export default InviteCohort;
src/views/Party/ShowParty.jsnew file+159
@@ -0,0 +1,159 @@
1+import React, { useMemo, useEffect, useCallback, useState } from 'react';
2+import { Grid, Flex, Input } from 'indigo-react';
3+import * as ob from 'urbit-ob';
4+import { Just } from 'folktale/maybe';
5+import cn from 'classnames';
6+
7+import { usePointCursor } from 'store/pointCursor';
8+import { usePointCache } from 'store/pointCache';
9+
10+import View from 'components/View';
11+import BarGraph from 'components/BarGraph';
12+import InviteSigilList from 'components/InviteSigilList';
13+import MaybeSigil from 'components/MaybeSigil';
14+import { matchBlinky } from 'components/Blinky';
15+import Chip from 'components/Chip';
16+import Crumbs from 'components/Crumbs';
17+
18+import { useLocalRouter } from 'lib/LocalRouter';
19+import useInvites from 'lib/useInvites';
20+import * as need from 'lib/need';
21+
22+import { ReactComponent as SearchIcon } from 'assets/search.svg';
23+
24+function PartyItem({ point, className }) {
25+ const { sentInvites } = useInvites(point);
26+ const patp = useMemo(() => ob.patp(point), [point]);
27+ const colors = ['#000000', '#FFFFFF'];
28+
29+ return (
30+ <Flex justify="between" className={cn('b1 b-gray2', className)}>
31+ <Flex.Item className="w9 h9">
32+ <MaybeSigil patp={Just(patp)} size={64} colors={colors} />
33+ </Flex.Item>
34+
35+ <Flex.Item
36+ flex={1}
37+ justify="evenly"
38+ as={Flex}
39+ col
40+ className="mono f6 ph4">
41+ <Flex.Item>{patp}</Flex.Item>
42+ <Flex.Item>{matchBlinky(sentInvites)} points invited</Flex.Item>
43+ </Flex.Item>
44+ </Flex>
45+ );
46+}
47+function PartyList({ points, className }) {
48+ const { syncInvites } = usePointCache();
49+
50+ const [query, setQuery] = useState('');
51+
52+ useEffect(() => {
53+ points.map(syncInvites);
54+ }, [points, syncInvites]);
55+
56+ const handleChange = useCallback(e => {
57+ setQuery(e.target.value);
58+ e.preventDefault();
59+ });
60+
61+ const visiblePoints = useMemo(
62+ () =>
63+ points.filter(p => {
64+ const patp = ob.patp(p).slice(1);
65+ return patp.startsWith(query);
66+ }),
67+ [query, points]
68+ );
69+
70+ return (
71+ <Grid gap={3} className={cn('mt1', className)}>
72+ <Grid.Item align="center" full as={Flex} className="b b-gray2 b1">
73+ <Flex.Item>
74+ <SearchIcon className="p2" />
75+ </Flex.Item>
76+
77+ <Flex.Item
78+ flex={1}
79+ as={'input'}
80+ value={query}
81+ onChange={handleChange}
82+ className="b-none"
83+ />
84+ </Grid.Item>
85+ <>
86+ {visiblePoints.map((p, idx) => (
87+ <Grid.Item key={p} half={(idx % 2) + 1} as={PartyItem} point={p} />
88+ ))}
89+ </>
90+ </Grid>
91+ );
92+}
93+
94+export default function ShowParty() {
95+ const { pop, push, names } = useLocalRouter();
96+
97+ const { pointCursor } = usePointCursor();
98+ const point = need.point(pointCursor);
99+
100+ const {
101+ acceptedInvites,
102+ acceptedPoints,
103+ pendingPoints,
104+ availableInvites,
105+ sentInvites,
106+ } = useInvites(point);
107+
108+ const _acceptedPoints = acceptedPoints.getOrElse([]);
109+ const _pendingPoints = pendingPoints.getOrElse([]);
110+ const _pendingInvites = _pendingPoints.length;
111+ const _acceptedInvites = _acceptedPoints.length;
112+ const _totalInvites = availableInvites
113+ .map(a => sentInvites.getOrElse(0) + a + _acceptedInvites)
114+ .getOrElse(0);
115+
116+ return (
117+ <View pop={pop} inset>
118+ <Grid gap={3}>
119+ <Grid.Item
120+ full
121+ as={Crumbs}
122+ className="mb1"
123+ routes={[
124+ { text: ob.patp(point), action: () => pop() },
125+ { text: 'Invite Group' },
126+ ]}
127+ />
128+
129+ <Grid.Item full>Invite Group</Grid.Item>
130+ <Grid.Item full>
131+ <Flex align="center">
132+ <Flex.Item>
133+ {_acceptedInvites} / {_totalInvites}
134+ </Flex.Item>
135+ {_pendingInvites && (
136+ <Flex.Item as={Chip} color="yellow">
137+ {_pendingInvites} pending
138+ </Flex.Item>
139+ )}
140+ </Flex>
141+ </Grid.Item>
142+ <Grid.Item
143+ full
144+ as={BarGraph}
145+ available={availableInvites}
146+ sent={sentInvites}
147+ accepted={acceptedInvites}
148+ />
149+ <Grid.Item
150+ full
151+ as={InviteSigilList}
152+ pendingPoints={pendingPoints}
153+ acceptedPoints={acceptedPoints}
154+ />
155+ <Grid.Item full as={PartyList} points={_acceptedPoints} />
156+ </Grid>
157+ </View>
158+ );
159+}
src/views/Point.js+28−44
@@ -1,10 +1,7 @@
11import React, { useCallback, useState } from 'react';
22import { Just, Nothing } from 'folktale/maybe';
3−import { Grid } from 'indigo-react';
3+import { Grid, Flex } from 'indigo-react';
44import { azimuth } from 'azimuth-js';
5−import cn from 'classnames';
6−import { take } from 'lodash';
7−import ob from 'urbit-ob';
85
96import { usePointCursor } from 'store/pointCursor';
107import { useWallet } from 'store/wallet';
@@ -17,7 +14,9 @@ import CopyButton from 'components/CopyButton';
1714import { matchBlinky } from 'components/Blinky';
1815import DownloadSigilButton from 'components/DownloadSigilButton';
1916import BarGraph from 'components/BarGraph';
17+import Chip from 'components/Chip';
2018import MaybeSigil from 'components/MaybeSigil';
19+import InviteSigilList from 'components/InviteSigilList';
2120
2221import * as need from 'lib/need';
2322import useInvites from 'lib/useInvites';
@@ -28,44 +27,6 @@ import useKeyfileGenerator from 'lib/useKeyfileGenerator';
2827
2928import InviteCohort from 'views/Invite/Cohort';
3029
31−const InviteSigilList = ({ className, pendingPoints, acceptedPoints }) => {
32− const _acceptedPoints = take(
33− acceptedPoints.getOrElse([]).map(x => Just(ob.patp(x))),
34− 6
35− );
36−
37− const _pendingPoints = take(
38− pendingPoints.getOrElse([]).map(x => Just(ob.patp(x))),
39− 6 - _acceptedPoints.length
40− );
41−
42− const empty = [
43− ...Array(
44− Math.max(6 - _acceptedPoints.length - _pendingPoints.length, 0)
45− ).keys(),
46− ].map(() => Nothing());
47−
48− const renderSigil = (points, colors, klassName) => {
49− return (
50− <>
51− {points.map((point, idx) => (
52− <div className={cn(klassName, 'h9 w9')}>
53− <MaybeSigil patp={point} size={50} colors={colors} />
54− </div>
55− ))}
56− </>
57− );
58− };
59−
60− return (
61− <div className={cn('flex justify-between', className)}>
62− {renderSigil(_acceptedPoints, ['#000000', '#FFFFFF'])}
63− {renderSigil(_pendingPoints, ['#ee892b', '#FFFFFF'])}
64− {renderSigil(empty, [], 'b1 b-black')}
65− </div>
66− );
67−};
68−
6930export default function Point() {
7031 const { pop, push, names } = useLocalRouter();
7132 const { pointCursor } = usePointCursor();
@@ -97,13 +58,19 @@ export default function Point() {
9758 );
9859
9960 const hasInvites = showInvites || availableInvites.getOrElse(0) !== 0;
100− //&&
61+
62+ const _totalInvites =
63+ sentInvites.getOrElse(0) + availableInvites.getOrElse(0);
64+ const _pendingInvites = pendingPoints.getOrElse([]).length;
65+ //
10166 // availableInvites.getOrElse(0) === 0
10267
10368 const goAdmin = useCallback(() => push(names.ADMIN), [push, names]);
10469
10570 const goInvite = useCallback(() => push(names.INVITE), [push, names]);
10671
72+ const goParty = useCallback(() => push(names.PARTY), [push, names]);
73+
10774 const goPartiesSetPoolSize = useCallback(
10875 () => push(names.PARTY_SET_POOL_SIZE),
10976 [push, names]
@@ -170,10 +137,27 @@ export default function Point() {
170137 <Grid gap={3}>
171138 {isPlanet && hasInvites && (
172139 <>
173− <Grid.Item full>
140+ <Grid.Item cols={[1, 11]}>
174141 Invite Group
175142 <br />
176143 </Grid.Item>
144+
145+ <Grid.Item className="t-right" onClick={goParty} cols={[11, 13]}>
146+ View
147+ </Grid.Item>
148+ <Grid.Item full>
149+ <Flex align="center">
150+ <Flex.Item>
151+ {acceptedInvites.getOrElse(0)} / {_totalInvites}
152+ </Flex.Item>
153+ {_pendingInvites > 0 && (
154+ <Flex.Item as={Chip} color="yellow">
155+ {_pendingInvites.getOrElse([]).length} pending
156+ </Flex.Item>
157+ )}
158+ </Flex>
159+ </Grid.Item>
160+
177161 {showInvites && (
178162 <>
179163 <Grid.Item