on 4c8c607 help sign in
bridge/src/views/Invite/Cohort.js
401 lines 11 kB history raw
1import React, { useMemo, useEffect, useCallback, useState } from 'react';
2import { Grid, Flex, Button } from 'indigo-react';
3import * as ob from 'urbit-ob';
4import { Just, Nothing } from 'folktale/maybe';
5import cn from 'classnames';
6import { delegatedSending } from 'azimuth-js';
8import { usePointCursor } from 'store/pointCursor';
9import { usePointCache } from 'store/pointCache';
10import { useWallet } from 'store/wallet';
12import View from 'components/View';
13import Tabs from 'components/Tabs';
14import BarGraph from 'components/BarGraph';
15import MaybeSigil from 'components/MaybeSigil';
16import { matchBlinky } from 'components/Blinky';
17import Chip from 'components/Chip';
18import Crumbs from 'components/Crumbs';
20import { useLocalRouter } from 'lib/LocalRouter';
21import useInvites from 'lib/useInvites';
22import * as need from 'lib/need';
23import * as wg from 'lib/walletgen';
25import Inviter from 'views/Invite/Inviter';
27import { ReactComponent as SearchIcon } from 'assets/search.svg';
28import CopyButton from 'components/CopyButton';
29import { useSyncForeignPoints } from 'lib/useSyncPoints';
30import { useNetwork } from 'store/network';
31import NavHeader from 'components/NavHeader';
33function SearchInput({ className, value, onChange }) {
34 return (
35 <Flex align="center" full className={cn('b b-gray2 b1', className)}>
36 <Flex.Item className="p2">
37 <SearchIcon />
38 </Flex.Item>
40 <Flex.Item
41 flex={1}
42 as={'input'}
43 value={value}
44 onChange={onChange}
45 className="b-none h7"
46 />
47 </Flex>
48 );
51function CohortMember({ point, pending = false, className, ...rest }) {
52 const { sentInvites } = useInvites(point);
53 const patp = useMemo(() => ob.patp(point), [point]);
54 const colors = pending ? ['#ee892b', '#FFFFFF'] : ['#000000', '#FFFFFF'];
56 const DetailText = useCallback(
57 () =>
58 pending ? 'Pending' : <> {matchBlinky(sentInvites)} points invited </>,
59 [pending, sentInvites]
60 );
62 return (
63 <Flex justify="between" className={cn('b1 b-gray2', className)} {...rest}>
64 <Flex.Item className="w9 h9">
65 <MaybeSigil patp={Just(patp)} size={64} colors={colors} />
66 </Flex.Item>
68 <Flex.Item
69 flex={1}
70 justify="evenly"
71 as={Flex}
72 col
73 className="mono f6 ph4">
74 <Flex.Item>{patp}</Flex.Item>
75 <Flex.Item className="gray4">
76 <DetailText />
77 </Flex.Item>
78 </Flex.Item>
79 </Flex>
80 );
82function CohortList({
83 acceptedPoints,
84 pendingPoints,
85 className,
86 onlyPending,
87 onSelectInvite,
88}) {
89 const { syncInvites } = usePointCache();
91 const [query, setQuery] = useState('');
93 useEffect(() => {
94 acceptedPoints.map(syncInvites);
95 }, [acceptedPoints, syncInvites]);
97 const handleChange = useCallback(
98 e => {
99 setQuery(e.target.value);
100 e.preventDefault();
101 },
102 [setQuery]
103 );
105 const filterPoints = useCallback(
106 points =>
107 points.filter(p => {
108 const patp = ob.patp(p).slice(1);
109 return patp.startsWith(query);
110 }),
111 [query]
112 );
113 const selectInvite = useCallback(
114 point => () => {
115 onSelectInvite(point);
116 },
117 [onSelectInvite]
118 );
119 const _pendingPoints = filterPoints(pendingPoints);
120 const _acceptedPoints = filterPoints(acceptedPoints);
122 const offset = onlyPending ? 0 : _acceptedPoints.length;
124 return (
125 <Grid gap={3} className={cn('mt4', className)}>
126 <Grid.Item full as={SearchInput} value={query} onChange={handleChange} />
127 <>
128 {!onlyPending &&
129 _acceptedPoints.map((p, idx) => (
130 <Grid.Item
131 key={p}
132 half={(idx % 2) + 1}
133 as={CohortMember}
134 point={p}
135 onClick={selectInvite(p)}
136 />
137 ))}
138 {_pendingPoints.map((p, idx) => (
139 <Grid.Item
140 key={p}
141 half={((offset + idx) % 2) + 1}
142 as={CohortMember}
143 point={p}
144 pending
145 onClick={selectInvite(p)}
146 />
147 ))}
148 </>
150 {_pendingPoints.length === 0 &&
151 (_acceptedPoints.length === 0 || onlyPending) && (
152 <Grid.Item full className="p4 t-center">
153 {' '}
154 No invites accepted yet.
155 </Grid.Item>
156 )}
157 </Grid>
158 );
161function hideIf(hider, hidden) {
162 return hider ? '●●●●●●●' : hidden;
165function 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 );
178 const [code, setCode] = useState(Nothing());
179 const [codeVisible, setCodeVisible] = useState(false);
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 );
201 if (owner.keys.address !== _details.transferProxy) {
202 setCode(Just(null));
203 } else {
204 setCode(Just(ticket));
205 }
206 };
207 fetchWallet();
208 }, [authToken, codeVisible, contracts, details, point]);
210 useEffect(() => {
211 setCodeVisible(false);
212 }, [point]);
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>
220 <Grid.Item cols={[4, 13]} className="mono mt-auto ">
221 {patp}
222 </Grid.Item>
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 )}
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 );
279export default function InviteCohort() {
280 const { pop } = useLocalRouter();
282 const { pointCursor } = usePointCursor();
283 const point = need.point(pointCursor);
285 const { syncDetails } = usePointCache();
287 const {
288 acceptedInvites,
289 acceptedPoints,
290 pendingPoints,
291 availableInvites,
292 sentInvites,
293 } = useInvites(point);
295 const [showInviteForm, setShowInviteForm] = useState(false);
296 const [tab, setTab] = useState(NAMES.ALL);
298 const _acceptedPoints = acceptedPoints.getOrElse([]);
299 const _pendingPoints = pendingPoints.getOrElse([]);
300 const _pendingInvites = _pendingPoints.length;
301 const _acceptedInvites = _acceptedPoints.length;
302 const _totalInvites = availableInvites
303 .map(a => sentInvites.getOrElse(0) + a)
304 .getOrElse(0);
306 const [selectedInvite, _setSelectedInvite] = useState();
308 useSyncForeignPoints([..._acceptedPoints, ..._pendingPoints]);
310 const setSelectedInvite = useCallback(
311 p => _setSelectedInvite(old => (old === p ? null : p)),
312 []
313 );
315 return (
316 <View pop={pop} inset>
317 <NavHeader>
318 <Crumbs
319 className="mb1"
320 routes={[
321 { text: ob.patp(point), action: () => pop() },
322 { text: 'Invite Group' },
323 ]}
324 />
325 </NavHeader>
326 <Grid gap={3}>
327 <Grid.Item full>Invite Group</Grid.Item>
328 <Grid.Item full>
329 <Flex align="center">
330 <Flex.Item>
331 {_acceptedInvites} / {_totalInvites}
332 </Flex.Item>
333 {_pendingInvites !== 0 && (
334 <Flex.Item as={Chip} className="bg-yellow4 white">
335 {_pendingInvites} pending
336 </Flex.Item>
337 )}
338 </Flex>
339 </Grid.Item>
340 <Grid.Item
341 full
342 as={BarGraph}
343 available={availableInvites}
344 sent={sentInvites}
345 accepted={acceptedInvites}
346 />
347 {availableInvites.getOrElse(0) !== 0 && !showInviteForm && (
348 <Grid.Item
349 full
350 solid
351 center
352 as={Button}
353 onClick={() => setShowInviteForm(true)}>
354 Add Members
355 </Grid.Item>
356 )}
357 {showInviteForm && <Inviter />}
359 {true && (
360 <Grid.Item
361 full
362 center
363 as={Tabs}
364 views={VIEWS}
365 options={OPTIONS}
366 currentTab={tab}
367 onTabChange={setTab}
368 acceptedPoints={_acceptedPoints}
369 pendingPoints={_pendingPoints}
370 onlyPending={tab === NAMES.PENDING}
371 onSelectInvite={setSelectedInvite}
372 />
373 )}
374 {selectedInvite && (
375 <Grid.Item
376 full
377 center
378 as={CohortMemberExpanded}
379 pending
380 point={selectedInvite}
381 onBack={() => setSelectedInvite(null)}
382 />
383 )}
384 </Grid>
385 </View>
386 );
388const NAMES = {
389 ALL: 'ALL',
390 PENDING: 'PENDING',
391};
393const VIEWS = {
394 [NAMES.PENDING]: CohortList,
395 [NAMES.ALL]: CohortList,
396};
398const OPTIONS = [
399 { text: 'All', value: NAMES.ALL },
400 { text: 'Pending', value: NAMES.PENDING },
401];