on master help sign in
add invite group detail view
44dc2feLiam Fitzgerald
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,
1573.justify-around {
1573.justify-around {
1574 justify-content: space-around; }
1574 justify-content: space-around; }
1575
1575
1576+.justify-evenly {
1577+ justify-content: space-evenly;
1578+}
1579+
1576.flex-center {
1580.flex-center {
1577 display: flex;
1581 display: flex;
1578 justify-content: center;
1582 justify-content: center;
src/lib/routeNames.js+1
@@ -20,4 +20,5 @@ export const ROUTE_NAMES = {
20 CREATE_GALAXY: Symbol('CREATE_GALAXY'),
20 CREATE_GALAXY: Symbol('CREATE_GALAXY'),
21 ISSUE_CHILD: Symbol('ISSUE_CHILD'),
21 ISSUE_CHILD: Symbol('ISSUE_CHILD'),
22 PARTY_SET_POOL_SIZE: Symbol('PARTY_SET_POOL_SIZE'),
22 PARTY_SET_POOL_SIZE: Symbol('PARTY_SET_POOL_SIZE'),
23+ PARTY: Symbol('PARTY'),
23};
24};
src/lib/router.js+2
@@ -10,6 +10,7 @@ import Invite from 'views/Invite';
10import Activate from 'views/Activate';
10import Activate from 'views/Activate';
11
11
12import PartySetPoolSize from 'views/Party/PartySetPoolSize';
12import PartySetPoolSize from 'views/Party/PartySetPoolSize';
13+import ShowParty from 'views/Party/ShowParty';
13import AcceptTransfer from 'views/AcceptTransfer';
14import AcceptTransfer from 'views/AcceptTransfer';
14import CancelTransfer from 'views/CancelTransfer';
15import CancelTransfer from 'views/CancelTransfer';
15
16
@@ -30,4 +31,5 @@ export const ROUTES = {
30 [ROUTE_NAMES.ACCEPT_TRANSFER]: AcceptTransfer,
31 [ROUTE_NAMES.ACCEPT_TRANSFER]: AcceptTransfer,
31 // TODO: replace this with deep link to AdminCancelTransfer
32 // TODO: replace this with deep link to AdminCancelTransfer
32 [ROUTE_NAMES.CANCEL_TRANSFER]: CancelTransfer,
33 [ROUTE_NAMES.CANCEL_TRANSFER]: CancelTransfer,
34+ [ROUTE_NAMES.PARTY]: ShowParty,
33};
35};
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 @@
1import React, { useCallback, useState } from 'react';
1import React, { useCallback, useState } from 'react';
2import { Just, Nothing } from 'folktale/maybe';
2import { Just, Nothing } from 'folktale/maybe';
3−import { Grid } from 'indigo-react';
3+import { Grid, Flex } from 'indigo-react';
4import { azimuth } from 'azimuth-js';
4import { azimuth } from 'azimuth-js';
5−import cn from 'classnames';
6−import { take } from 'lodash';
7−import ob from 'urbit-ob';
8
5
9import { usePointCursor } from 'store/pointCursor';
6import { usePointCursor } from 'store/pointCursor';
10import { useWallet } from 'store/wallet';
7import { useWallet } from 'store/wallet';
@@ -17,7 +14,9 @@ import CopyButton from 'components/CopyButton';
17import { matchBlinky } from 'components/Blinky';
14import { matchBlinky } from 'components/Blinky';
18import DownloadSigilButton from 'components/DownloadSigilButton';
15import DownloadSigilButton from 'components/DownloadSigilButton';
19import BarGraph from 'components/BarGraph';
16import BarGraph from 'components/BarGraph';
17+import Chip from 'components/Chip';
20import MaybeSigil from 'components/MaybeSigil';
18import MaybeSigil from 'components/MaybeSigil';
19+import InviteSigilList from 'components/InviteSigilList';
21
20
22import * as need from 'lib/need';
21import * as need from 'lib/need';
23import useInvites from 'lib/useInvites';
22import useInvites from 'lib/useInvites';
@@ -28,44 +27,6 @@ import useKeyfileGenerator from 'lib/useKeyfileGenerator';
28
27
29import InviteCohort from 'views/Invite/Cohort';
28import InviteCohort from 'views/Invite/Cohort';
30
29
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−
69export default function Point() {
30export default function Point() {
70 const { pop, push, names } = useLocalRouter();
31 const { pop, push, names } = useLocalRouter();
71 const { pointCursor } = usePointCursor();
32 const { pointCursor } = usePointCursor();
@@ -97,13 +58,19 @@ export default function Point() {
97 );
58 );
98
59
99 const hasInvites = showInvites || availableInvites.getOrElse(0) !== 0;
60 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+ //
101 // availableInvites.getOrElse(0) === 0
66 // availableInvites.getOrElse(0) === 0
102
67
103 const goAdmin = useCallback(() => push(names.ADMIN), [push, names]);
68 const goAdmin = useCallback(() => push(names.ADMIN), [push, names]);
104
69
105 const goInvite = useCallback(() => push(names.INVITE), [push, names]);
70 const goInvite = useCallback(() => push(names.INVITE), [push, names]);
106
71
72+ const goParty = useCallback(() => push(names.PARTY), [push, names]);
73+
107 const goPartiesSetPoolSize = useCallback(
74 const goPartiesSetPoolSize = useCallback(
108 () => push(names.PARTY_SET_POOL_SIZE),
75 () => push(names.PARTY_SET_POOL_SIZE),
109 [push, names]
76 [push, names]
@@ -170,10 +137,27 @@ export default function Point() {
170 <Grid gap={3}>
137 <Grid gap={3}>
171 {isPlanet && hasInvites && (
138 {isPlanet && hasInvites && (
172 <>
139 <>
173− <Grid.Item full>
140+ <Grid.Item cols={[1, 11]}>
174 Invite Group
141 Invite Group
175 <br />
142 <br />
176 </Grid.Item>
143 </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+
177 {showInvites && (
161 {showInvites && (
178 <>
162 <>
179 <Grid.Item
163 <Grid.Item