on 4b0ca21 help sign in
bridge/src/views/Invite/Cohort.js
398 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';
7import { usePointCursor } from 'store/pointCursor';
8import { usePointCache } from 'store/pointCache';
9import { useWallet } from 'store/wallet';
11import View from 'components/View';
12import Tabs from 'components/Tabs';
13import BarGraph from 'components/BarGraph';
14import MaybeSigil from 'components/MaybeSigil';
15import { matchBlinky } from 'components/Blinky';
16import Chip from 'components/Chip';
17import Crumbs from 'components/Crumbs';
19import { useLocalRouter } from 'lib/LocalRouter';
20import useInvites from 'lib/useInvites';
21import * as need from 'lib/need';
22import * as wg from 'lib/walletgen';
24import Inviter from 'views/Invite/Inviter';
26import { ReactComponent as SearchIcon } from 'assets/search.svg';
27import CopyButton from 'components/CopyButton';
28import { useSyncDetails } from 'lib/useSyncPoints';
29import { useNetwork } from 'store/network';
30import NavHeader from 'components/NavHeader';
32function SearchInput({ className, value, onChange }) {
33 return (
34 <Flex align="center" full className={cn('b b-gray2 b1', className)}>
35 <Flex.Item className="p2">
36 <SearchIcon />
37 </Flex.Item>
39 <Flex.Item
40 flex={1}
41 as={'input'}
42 value={value}
43 onChange={onChange}
44 className="b-none h7"
45 />
46 </Flex>
47 );
50function CohortMember({ point, pending = false, className, ...rest }) {
51 const { sentInvites } = useInvites(point);
52 const patp = useMemo(() => ob.patp(point), [point]);
53 const colors = pending ? ['#ee892b', '#FFFFFF'] : ['#000000', '#FFFFFF'];
55 const DetailText = useCallback(
56 () =>
57 pending ? 'Pending' : <> {matchBlinky(sentInvites)} points invited </>,
58 [pending, sentInvites]
59 );
61 return (
62 <Flex justify="between" className={cn('b1 b-gray2', className)} {...rest}>
63 <Flex.Item className="w9 h9">
64 <MaybeSigil patp={Just(patp)} size={64} colors={colors} />
65 </Flex.Item>
67 <Flex.Item
68 flex={1}
69 justify="evenly"
70 as={Flex}
71 col
72 className="mono f6 ph4">
73 <Flex.Item>{patp}</Flex.Item>
74 <Flex.Item className="gray4">
75 <DetailText />
76 </Flex.Item>
77 </Flex.Item>
78 </Flex>
79 );
81function CohortList({
82 acceptedPoints,
83 pendingPoints,
84 className,
85 onlyPending,
86 onSelectInvite,
87}) {
88 const { syncInvites } = usePointCache();
90 const [query, setQuery] = useState('');
92 useEffect(() => {
93 acceptedPoints.map(syncInvites);
94 }, [acceptedPoints, syncInvites]);
96 const handleChange = useCallback(
97 e => {
98 setQuery(e.target.value);
99 e.preventDefault();
100 },
101 [setQuery]
102 );
104 const filterPoints = useCallback(
105 points =>
106 points.filter(p => {
107 const patp = ob.patp(p).slice(1);
108 return patp.startsWith(query);
109 }),
110 [query]
111 );
112 const selectInvite = useCallback(
113 point => () => {
114 onSelectInvite(point);
115 },
116 [onSelectInvite]
117 );
118 const _pendingPoints = filterPoints(pendingPoints);
119 const _acceptedPoints = filterPoints(acceptedPoints);
121 const offset = onlyPending ? 0 : _acceptedPoints.length;
123 return (
124 <Grid gap={3} className={cn('mt4', className)}>
125 <Grid.Item full as={SearchInput} value={query} onChange={handleChange} />
126 <>
127 {!onlyPending &&
128 _acceptedPoints.map((p, idx) => (
129 <Grid.Item
130 key={p}
131 half={(idx % 2) + 1}
132 as={CohortMember}
133 point={p}
134 onClick={selectInvite(p)}
135 />
136 ))}
137 {_pendingPoints.map((p, idx) => (
138 <Grid.Item
139 key={p}
140 half={((offset + idx) % 2) + 1}
141 as={CohortMember}
142 point={p}
143 pending
144 onClick={selectInvite(p)}
145 />
146 ))}
147 </>
149 {_pendingPoints.length === 0 &&
150 (_acceptedPoints.length === 0 || onlyPending) && (
151 <Grid.Item full className="p4 t-center">
152 {' '}
153 No invites accepted yet.
154 </Grid.Item>
155 )}
156 </Grid>
157 );
160function hideIf(hider, hidden) {
161 return hider ? '●●●●●●●' : hidden;
164function CohortMemberExpanded({ point, className, ...rest }) {
165 const { sentInvites, acceptedInvites } = useInvites(point);
166 const { getDetails } = usePointCache();
167 const { authToken } = useWallet();
168 const { contracts } = useNetwork();
169 const details = getDetails(point);
170 const { active } = details.getOrElse({});
171 const patp = useMemo(() => ob.patp(point), [point]);
172 const colors = useMemo(
173 () => (!active ? ['#ee892b', '#FFFFFF'] : ['#000000', '#FFFFFF']),
174 [active]
175 );
177 const [code, setCode] = useState(Nothing());
178 const [codeVisible, setCodeVisible] = useState(false);
180 useEffect(() => {
181 const fetchWallet = async () => {
182 const _authToken = authToken.getOrElse(null);
183 const _details = details.getOrElse(null);
184 const _contracts = contracts.getOrElse(null);
185 if (
186 !_authToken ||
187 !_details ||
188 _details.active ||
189 !_contracts ||
190 !codeVisible
191 ) {
192 setCode(Nothing());
193 return;
194 }
195 const { ticket, owner } = await wg.generateTemporaryDeterministicWallet(
196 point,
197 _authToken
198 );
200 if (owner.keys.address !== _details.transferProxy) {
201 setCode(Just(null));
202 } else {
203 setCode(Just(ticket));
204 }
205 };
206 fetchWallet();
207 }, [authToken, codeVisible, contracts, details, point]);
209 useEffect(() => {
210 setCodeVisible(false);
211 }, [point]);
213 return (
214 <Grid justify="between" className={cn('b1 b-gray2', className)} {...rest}>
215 <Grid.Item rows={[1, 4]} cols={[1, 4]} className="h10 w10">
216 <MaybeSigil patp={Just(patp)} size={64} colors={colors} />
217 </Grid.Item>
219 <Grid.Item cols={[4, 13]} className="mono mt-auto ">
220 {patp}
221 </Grid.Item>
223 {!active && (
224 <>
225 <Grid.Item
226 cols={[4, 13]}
227 className="mt-auto mb1 f6 gray4"
228 as={Flex}
229 col>
230 Invite Code
231 </Grid.Item>
232 <Grid.Item cols={[4, 10]} className="mb-auto mt1 f6 gray4">
233 {(code.getOrElse(true) === null &&
234 'This invite code cannot be recovered') ||
235 hideIf(!codeVisible, matchBlinky(code))}
236 </Grid.Item>
237 {!codeVisible && (
238 <Grid.Item
239 cols={[10, 13]}
240 className="mh-auto f6 underline pointer"
241 onClick={() => setCodeVisible(true)}>
242 Show
243 </Grid.Item>
244 )}
245 {codeVisible && code.getOrElse('') !== null && (
246 <Grid.Item
247 className="mh-auto f6 t-right"
248 cols={[10, 13]}
249 as={CopyButton}
250 text={code.getOrElse('')}
251 />
252 )}
253 </>
254 )}
256 {active && (
257 <>
258 <Grid.Item
259 cols={[4, 13]}
260 className="mt-auto mb1 f6 gray4"
261 as={Flex}
262 col>
263 {matchBlinky(sentInvites)} invites sent
264 </Grid.Item>
265 <Grid.Item
266 cols={[4, 13]}
267 className="mb-auto mt1 f6 gray4"
268 as={Flex}
269 col>
270 {matchBlinky(acceptedInvites)} invites accepted
271 </Grid.Item>
272 </>
273 )}
274 </Grid>
275 );
278export default function InviteCohort() {
279 const { pop } = useLocalRouter();
281 const { pointCursor } = usePointCursor();
282 const point = need.point(pointCursor);
284 const {
285 acceptedInvites,
286 acceptedPoints,
287 pendingPoints,
288 availableInvites,
289 sentInvites,
290 } = useInvites(point);
292 const [showInviteForm, setShowInviteForm] = useState(false);
293 const [tab, setTab] = useState(NAMES.ALL);
295 const _acceptedPoints = acceptedPoints.getOrElse([]);
296 const _pendingPoints = pendingPoints.getOrElse([]);
297 const _pendingInvites = _pendingPoints.length;
298 const _acceptedInvites = _acceptedPoints.length;
299 const _totalInvites = availableInvites
300 .map(a => sentInvites.getOrElse(0) + a)
301 .getOrElse(0);
303 const [selectedInvite, _setSelectedInvite] = useState();
305 useSyncDetails([..._acceptedPoints, ..._pendingPoints]);
307 const setSelectedInvite = useCallback(
308 p => _setSelectedInvite(old => (old === p ? null : p)),
309 []
310 );
312 return (
313 <View pop={pop} inset>
314 <NavHeader>
315 <Crumbs
316 className="mb1"
317 routes={[
318 { text: ob.patp(point), action: () => pop() },
319 { text: 'Invite Group' },
320 ]}
321 />
322 </NavHeader>
323 <Grid gap={3}>
324 <Grid.Item full>Invite Group</Grid.Item>
325 <Grid.Item full>
326 <Flex align="center">
327 <Flex.Item>
328 {_acceptedInvites} / {_totalInvites}
329 </Flex.Item>
330 {_pendingInvites !== 0 && (
331 <Flex.Item as={Chip} className="bg-yellow4 white">
332 {_pendingInvites} pending
333 </Flex.Item>
334 )}
335 </Flex>
336 </Grid.Item>
337 <Grid.Item
338 full
339 as={BarGraph}
340 available={availableInvites}
341 sent={sentInvites}
342 accepted={acceptedInvites}
343 />
344 {availableInvites.getOrElse(0) !== 0 && !showInviteForm && (
345 <Grid.Item
346 full
347 solid
348 center
349 as={Button}
350 onClick={() => setShowInviteForm(true)}>
351 Add Members
352 </Grid.Item>
353 )}
354 {showInviteForm && <Inviter />}
356 {true && (
357 <Grid.Item
358 full
359 center
360 as={Tabs}
361 views={VIEWS}
362 options={OPTIONS}
363 currentTab={tab}
364 onTabChange={setTab}
365 acceptedPoints={_acceptedPoints}
366 pendingPoints={_pendingPoints}
367 onlyPending={tab === NAMES.PENDING}
368 onSelectInvite={setSelectedInvite}
369 />
370 )}
371 {selectedInvite && (
372 <Grid.Item
373 full
374 center
375 as={CohortMemberExpanded}
376 pending
377 point={selectedInvite}
378 onBack={() => setSelectedInvite(null)}
379 />
380 )}
381 </Grid>
382 </View>
383 );
385const NAMES = {
386 ALL: 'ALL',
387 PENDING: 'PENDING',
388};
390const VIEWS = {
391 [NAMES.PENDING]: CohortList,
392 [NAMES.ALL]: CohortList,
393};
395const OPTIONS = [
396 { text: 'All', value: NAMES.ALL },
397 { text: 'Pending', value: NAMES.PENDING },
398];