@@ -1,11 +1,13 @@
11import React, { useMemo, useEffect, useCallback, useState } from 'react';
22import { Grid, Flex, Button } from 'indigo-react';
33import * as ob from 'urbit-ob';
4−import { Just } from 'folktale/maybe';
4+import { Just, Nothing } from 'folktale/maybe';
55import cn from 'classnames';
6+import { delegatedSending } from 'azimuth-js';
67
78import { usePointCursor } from 'store/pointCursor';
89import { usePointCache } from 'store/pointCache';
10+import { useWallet } from 'store/wallet';
911
1012import View from 'components/View';
1113import Tabs from 'components/Tabs';
@@ -19,10 +21,14 @@ import Crumbs from 'components/Crumbs';
1921import { useLocalRouter } from 'lib/LocalRouter';
2022import useInvites from 'lib/useInvites';
2123import * as need from 'lib/need';
24+import * as wg from 'lib/walletgen';
2225
2326import Inviter from 'views/Invite/Inviter';
2427
2528import { ReactComponent as SearchIcon } from 'assets/search.svg';
29+import CopyButton from 'components/CopyButton';
30+import { useSyncForeignPoints } from 'lib/useSyncPoints';
31+import { useNetwork } from 'store/network';
2632
2733function SearchInput({ className, value, onChange }) {
2834 return (
@@ -42,7 +48,7 @@ function SearchInput({ className, value, onChange }) {
4248 );
4349}
4450
45−function CohortMember({ point, pending = false, className }) {
51+function CohortMember({ point, pending = false, className, ...rest }) {
4652 const { sentInvites } = useInvites(point);
4753 const patp = useMemo(() => ob.patp(point), [point]);
4854 const colors = pending ? ['#ee892b', '#FFFFFF'] : ['#000000', '#FFFFFF'];
@@ -54,7 +60,7 @@ function CohortMember({ point, pending = false, className }) {
5460 );
5561
5662 return (
57− <Flex justify="between" className={cn('b1 b-gray2', className)}>
63+ <Flex justify="between" className={cn('b1 b-gray2', className)} {...rest}>
5864 <Flex.Item className="w9 h9">
5965 <MaybeSigil patp={Just(patp)} size={64} colors={colors} />
6066 </Flex.Item>
@@ -73,7 +79,13 @@ function CohortMember({ point, pending = false, className }) {
7379 </Flex>
7480 );
7581}
76−function CohortList({ acceptedPoints, pendingPoints, className, onlyPending }) {
82+function CohortList({
83+ acceptedPoints,
84+ pendingPoints,
85+ className,
86+ onlyPending,
87+ onSelectInvite,
88+}) {
7789 const { syncInvites } = usePointCache();
7890
7991 const [query, setQuery] = useState('');
@@ -98,7 +110,12 @@ function CohortList({ acceptedPoints, pendingPoints, className, onlyPending }) {
98110 }),
99111 [query]
100112 );
101−
113+ const selectInvite = useCallback(
114+ point => () => {
115+ onSelectInvite(point);
116+ },
117+ [onSelectInvite]
118+ );
102119 const _pendingPoints = filterPoints(pendingPoints);
103120 const _acceptedPoints = filterPoints(acceptedPoints);
104121
@@ -115,6 +132,7 @@ function CohortList({ acceptedPoints, pendingPoints, className, onlyPending }) {
115132 half={(idx % 2) + 1}
116133 as={CohortMember}
117134 point={p}
135+ onClick={selectInvite(p)}
118136 />
119137 ))}
120138 {_pendingPoints.map((p, idx) => (
@@ -124,6 +142,7 @@ function CohortList({ acceptedPoints, pendingPoints, className, onlyPending }) {
124142 as={CohortMember}
125143 point={p}
126144 pending
145+ onClick={selectInvite(p)}
127146 />
128147 ))}
129148 </>
@@ -139,12 +158,132 @@ function CohortList({ acceptedPoints, pendingPoints, className, onlyPending }) {
139158 );
140159}
141160
161+function hideIf(hider, hidden) {
162+ return hider ? '●●●●●●●' : hidden;
163+}
164+
165+function CohortMemberExpanded({ point, className, ...rest }) {
166+ const { sentInvites, acceptedInvites } = useInvites(point);
167+ const { getDetails } = usePointCache();
168+ const { authToken } = useWallet();
169+ const { contracts } = useNetwork();
170+ const details = getDetails(point);
171+ const { active } = details.getOrElse({});
172+ const patp = useMemo(() => ob.patp(point), [point]);
173+ const colors = useMemo(
174+ () => (!active ? ['#ee892b', '#FFFFFF'] : ['#000000', '#FFFFFF']),
175+ [active]
176+ );
177+
178+ const [code, setCode] = useState(Nothing());
179+ const [codeVisible, setCodeVisible] = useState(false);
180+
181+ useEffect(() => {
182+ const fetchWallet = async () => {
183+ const _authToken = authToken.getOrElse(null);
184+ const _details = details.getOrElse(null);
185+ const _contracts = contracts.getOrElse(null);
186+ if (
187+ !_authToken ||
188+ !_details ||
189+ _details.active ||
190+ !_contracts ||
191+ !codeVisible
192+ ) {
193+ setCode(Nothing());
194+ return;
195+ }
196+ const { ticket, owner } = await wg.generateTemporaryDeterministicWallet(
197+ point,
198+ _authToken
199+ );
200+
201+ if (owner.keys.address !== _details.transferProxy) {
202+ setCode(Just(null));
203+ } else {
204+ setCode(Just(ticket));
205+ }
206+ };
207+ fetchWallet();
208+ }, [codeVisible]);
209+
210+ useEffect(() => {
211+ setCodeVisible(false);
212+ }, [point]);
213+
214+ return (
215+ <Grid justify="between" className={cn('b1 b-gray2', className)} {...rest}>
216+ <Grid.Item rows={[1, 4]} cols={[1, 4]} className="h10 w10">
217+ <MaybeSigil patp={Just(patp)} size={64} colors={colors} />
218+ </Grid.Item>
219+
220+ <Grid.Item cols={[4, 13]} className="mono mt-auto ">
221+ {patp}
222+ </Grid.Item>
223+
224+ {!active && (
225+ <>
226+ <Grid.Item
227+ cols={[4, 13]}
228+ className="mt-auto mb1 f6 gray4"
229+ as={Flex}
230+ col>
231+ Invite Code
232+ </Grid.Item>
233+ <Grid.Item cols={[4, 10]} className="mb-auto mt1 f6 gray4">
234+ {(code.getOrElse(true) === null &&
235+ 'This invite code cannot be recovered') ||
236+ hideIf(!codeVisible, matchBlinky(code))}
237+ </Grid.Item>
238+ {!codeVisible && (
239+ <Grid.Item
240+ cols={[10, 13]}
241+ className="mh-auto f6 underline pointer"
242+ onClick={() => setCodeVisible(true)}>
243+ Show
244+ </Grid.Item>
245+ )}
246+ {codeVisible && code.getOrElse('') !== null && (
247+ <Grid.Item
248+ className="mh-auto f6 t-right"
249+ cols={[10, 13]}
250+ as={CopyButton}
251+ text={code.getOrElse('')}
252+ />
253+ )}
254+ </>
255+ )}
256+
257+ {active && (
258+ <>
259+ <Grid.Item
260+ cols={[4, 13]}
261+ className="mt-auto mb1 f6 gray4"
262+ as={Flex}
263+ col>
264+ {matchBlinky(sentInvites)} invites sent
265+ </Grid.Item>
266+ <Grid.Item
267+ cols={[4, 13]}
268+ className="mb-auto mt1 f6 gray4"
269+ as={Flex}
270+ col>
271+ {matchBlinky(acceptedInvites)} invites accepted
272+ </Grid.Item>
273+ </>
274+ )}
275+ </Grid>
276+ );
277+}
278+
142279export default function InviteCohort() {
143280 const { pop } = useLocalRouter();
144281
145282 const { pointCursor } = usePointCursor();
146283 const point = need.point(pointCursor);
147284
285+ const { syncDetails } = usePointCache();
286+
148287 const {
149288 acceptedInvites,
150289 acceptedPoints,
@@ -164,6 +303,15 @@ export default function InviteCohort() {
164303 .map(a => sentInvites.getOrElse(0) + a)
165304 .getOrElse(0);
166305
306+ const [selectedInvite, _setSelectedInvite] = useState();
307+
308+ useSyncForeignPoints([..._acceptedPoints, ..._pendingPoints]);
309+
310+ const setSelectedInvite = useCallback(
311+ p => _setSelectedInvite(old => (old === p ? null : p)),
312+ []
313+ );
314+
167315 return (
168316 <View pop={pop} inset>
169317 <Grid gap={3}>
@@ -209,18 +357,31 @@ export default function InviteCohort() {
209357 )}
210358 {showInviteForm && <Inviter />}
211359
212− <Grid.Item
213− full
214− center
215− as={Tabs}
216− views={VIEWS}
217− options={OPTIONS}
218− currentTab={tab}
219− onTabChange={setTab}
220− acceptedPoints={_acceptedPoints}
221− pendingPoints={_pendingPoints}
222− onlyPending={tab === NAMES.PENDING}
223− />
360+ {true && (
361+ <Grid.Item
362+ full
363+ center
364+ as={Tabs}
365+ views={VIEWS}
366+ options={OPTIONS}
367+ currentTab={tab}
368+ onTabChange={setTab}
369+ acceptedPoints={_acceptedPoints}
370+ pendingPoints={_pendingPoints}
371+ onlyPending={tab === NAMES.PENDING}
372+ onSelectInvite={setSelectedInvite}
373+ />
374+ )}
375+ {selectedInvite && (
376+ <Grid.Item
377+ full
378+ center
379+ as={CohortMemberExpanded}
380+ pending
381+ point={selectedInvite}
382+ onBack={() => setSelectedInvite(null)}
383+ />
384+ )}
224385 </Grid>
225386 </View>
226387 );