on ec040ad help sign in
bridge/src/views/Points.js
407 lines 12 kB history raw
1import React, { useCallback, useEffect, useMemo } from 'react';
2import { Just, Nothing } from 'folktale/maybe';
3import { Grid, H5, HelpText, LinkButton, Flex } from 'indigo-react';
4import { get } from 'lodash';
6import { useHistory } from 'store/history';
7import { useWallet } from 'store/wallet';
8import { usePointCache } from 'store/pointCache';
9import { usePointCursor } from 'store/pointCursor';
10import { useStarReleaseCache } from 'store/starRelease';
12import * as need from 'lib/need';
13import { isZeroAddress, abbreviateAddress } from 'lib/utils/address';
14import useIsEclipticOwner from 'lib/useIsEclipticOwner';
15import { useSyncDetails } from 'lib/useSyncPoints';
16import useRejectedIncomingPointTransfers from 'lib/useRejectedIncomingPointTransfers';
17import { pluralize } from 'lib/pluralize';
18import newGithubIssueUrl from 'new-github-issue-url';
20import View from 'components/View';
21import Blinky from 'components/Blinky';
22import Passport from 'components/Passport';
23import Footer from 'components/Footer';
24import { ForwardButton } from 'components/Buttons';
25import CopiableAddress from 'components/copiable/CopiableAddress';
26import NavHeader from 'components/NavHeader';
27import L2PointHeader from 'components/L2/Headers/L2PointHeader';
29const maybeGetResult = (obj, key, defaultValue) =>
30 obj.matchWith({
31 Nothing: () => defaultValue,
32 Just: p =>
33 p.value.matchWith({
34 Ok: r => get(r.value, key, defaultValue),
35 Error: e => defaultValue,
36 }),
37 });
39const hasTransferProxy = details => !isZeroAddress(details.transferProxy);
41const PointList = function({
42 points,
43 className,
44 actions,
45 locked = false,
46 ...rest
47}) {
48 const { setPointCursor } = usePointCursor();
49 const { push, names } = useHistory();
51 return (
52 <Grid gap={4} className={className}>
53 {points.map((point, i) => (
54 <Grid.Item
55 key={point}
56 className={`full fourth-${(i % 4) + 1}-md fourth-${(i % 4) + 1}-lg`}>
57 <Flex col>
58 <Passport.Mini
59 locked={locked}
60 point={point}
61 onClick={() => {
62 setPointCursor(Just(point));
63 push(names.POINT);
64 }}
65 {...rest}
66 />
67 {actions && (
68 <Flex.Item className="mt2">{actions(point, i)}</Flex.Item>
69 )}
70 </Flex>
71 </Grid.Item>
72 ))}
73 </Grid>
74 );
75};
77function ActionButtons({ actions = [] }) {
78 return (
79 <Flex row>
80 {actions.map(action => (
81 <Flex.Item
82 key={action.text}
83 as={LinkButton}
84 className="mr3"
85 onClick={action.onClick}>
86 {action.text}
87 </Flex.Item>
88 ))}
89 </Flex>
90 );
93export default function Points() {
94 const { wallet } = useWallet();
95 const { pop, push, popAndPush, names } = useHistory();
96 const { setPointCursor } = usePointCursor();
97 const { controlledPoints, getDetails } = usePointCache();
98 const isEclipticOwner = useIsEclipticOwner();
99 const [
100 rejectedPoints,
101 addRejectedPoint,
102 ] = useRejectedIncomingPointTransfers();
103 const { syncStarReleaseDetails, starReleaseDetails } = useStarReleaseCache();
105 const maybeOutgoingPoints = useMemo(
106 () =>
107 controlledPoints.chain(points =>
108 points.matchWith({
109 Error: () => Nothing(),
110 Ok: c => {
111 const points = c.value.ownedPoints.map(point =>
112 getDetails(point).chain(details =>
113 Just({ point: point, has: hasTransferProxy(details) })
114 )
115 );
116 // if we have details for every point,
117 // return the array of pending transfers.
118 if (points.every(p => Just.hasInstance(p))) {
119 const outgoing = points
120 .filter(p => p.value.has)
121 .map(p => p.value.point);
122 return Just(outgoing);
123 } else {
124 return Nothing();
125 }
126 },
127 })
128 ),
129 [getDetails, controlledPoints]
130 );
132 // if we can only interact with a single point, forget about the existence
133 // of this page and jump to the point page.
134 // if there are any pending transfers, incoming or outgoing, stay on this
135 // page, because those can only be completed/cancelled here.
136 useEffect(() => {
137 if (
138 Nothing.hasInstance(maybeOutgoingPoints) ||
139 Nothing.hasInstance(starReleaseDetails)
140 ) {
141 return;
142 }
143 controlledPoints.matchWith({
144 Nothing: () => null,
145 Just: r => {
146 r.value.matchWith({
147 Error: () => null,
148 Ok: c => {
149 let all = [
150 ...c.value.ownedPoints,
151 ...c.value.votingPoints,
152 ...c.value.managingPoints,
153 ...c.value.spawningPoints,
154 ];
155 const incoming = c.value.incomingPoints.filter(
156 p => !rejectedPoints.includes(p)
157 );
158 if (
159 all.length === 1 &&
160 incoming.length === 0 &&
161 maybeOutgoingPoints.value.length === 0 &&
162 (starReleaseDetails.value === null ||
163 starReleaseDetails.value.total === 0)
164 ) {
165 setPointCursor(Just(all[0]));
166 popAndPush(names.POINT);
167 }
168 },
169 });
170 },
171 });
172 }, [
173 controlledPoints,
174 rejectedPoints,
175 maybeOutgoingPoints,
176 setPointCursor,
177 popAndPush,
178 names,
179 starReleaseDetails,
180 ]);
182 const address = need.addressFromWallet(wallet);
184 const loading = Nothing.hasInstance(controlledPoints);
186 const ownedPoints = maybeGetResult(controlledPoints, 'ownedPoints', []);
187 const incomingPoints = maybeGetResult(
188 controlledPoints,
189 'incomingPoints',
190 []
191 ).filter(point => !rejectedPoints.includes(point));
192 const managingPoints = maybeGetResult(controlledPoints, 'managingPoints', []);
193 const votingPoints = maybeGetResult(controlledPoints, 'votingPoints', []);
194 const spawningPoints = maybeGetResult(controlledPoints, 'spawningPoints', []);
195 const outgoingPoints = maybeOutgoingPoints.getOrElse([]);
197 const allPoints = [
198 ...ownedPoints.filter(p => !outgoingPoints.includes(p)),
199 ...managingPoints,
200 ...votingPoints,
201 ...spawningPoints,
202 ].sort((a, b) => Number(a) - Number(b));
204 const displayEmptyState =
205 !loading && incomingPoints.length === 0 && allPoints.length === 0;
207 const starReleasing = starReleaseDetails
208 .map(s => (s ? s.total > 0 : false))
209 .getOrElse(false);
211 useEffect(() => {
212 syncStarReleaseDetails();
213 }, [syncStarReleaseDetails]);
215 // sync display details for known points
216 useSyncDetails(ownedPoints);
218 const goCreateGalaxy = useCallback(() => push(names.CREATE_GALAXY), [
219 names.CREATE_GALAXY,
220 push,
221 ]);
223 const goStarRelease = useCallback(() => push(names.STAR_RELEASE), [
224 names.STAR_RELEASE,
225 push,
226 ]);
228 // if we got an error result, we should display it, instead of showing
229 // a potentially inaccurately empty point list.
230 //
231 if (
232 Just.hasInstance(controlledPoints) &&
233 controlledPoints.value.matchWith({
234 Error: e => true,
235 Ok: v => false,
236 })
237 ) {
238 const url = newGithubIssueUrl({
239 user: 'urbit',
240 repo: 'bridge',
241 title: controlledPoints.value.value,
242 body: `<!-- Please provide some context. Do you have Metamask installed? What login method did you use? -->`,
243 });
244 return (
245 <View inset>
246 <Grid>
247 <Grid.Item full as={HelpText} className="mt8 t-center">
248 {controlledPoints.value.value}
249 <br />
250 Try reloading the page. If this problem persists, please{' '}
251 <a href={url}>file an issue on GitHub</a>.
252 </Grid.Item>
253 </Grid>
254 </View>
255 );
256 }
258 if (
259 loading ||
260 (allPoints.length === 1 &&
261 incomingPoints.length === 0 &&
262 outgoingPoints.length === 0 &&
263 !starReleasing)
264 ) {
265 return (
266 <View inset>
267 <Grid>
268 <Grid.Item full as={HelpText} className="mt8 t-center">
269 <Blinky /> Loading...
270 </Grid.Item>
271 </Grid>
272 </View>
273 );
274 }
276 return (
277 <View
278 inset
279 pop={pop}
280 hideBack
281 header={<L2PointHeader hideTimer hideInvites />}>
282 <NavHeader>
283 <CopiableAddress
284 text={address}
285 className="f6 mono gray4 mb4 us-none pointer">
286 {abbreviateAddress(address)}
287 </CopiableAddress>
288 </NavHeader>
289 <Grid>
290 {displayEmptyState && (
291 <Grid.Item full as={HelpText} className="mt8 t-center">
292 No points to display. This wallet is not the owner or proxy for any
293 points.
294 </Grid.Item>
295 )}
297 {incomingPoints.length > 0 && (
298 <Grid.Item full as={Grid} gap={1} className="mb6">
299 <Grid.Item full as={H5}>
300 {pluralize(
301 incomingPoints.length,
302 'Incoming Transfer',
303 'Incoming Transfers'
304 )}
305 </Grid.Item>
306 <Grid.Item
307 full
308 as={PointList}
309 points={incomingPoints}
310 actions={(point, i) => (
311 <ActionButtons
312 actions={[
313 {
314 text: 'Accept',
315 onClick: () => {
316 setPointCursor(Just(point));
317 push(names.ACCEPT_TRANSFER);
318 },
319 },
320 {
321 text: 'Reject',
322 onClick: () => {
323 addRejectedPoint(point);
324 },
325 },
326 ]}
327 />
328 )}
329 inverted
330 />
331 </Grid.Item>
332 )}
334 {outgoingPoints.length > 0 && (
335 <Grid.Item full as={Grid} gap={1} className="mb6">
336 <Grid.Item full as={H5}>
337 {pluralize(
338 outgoingPoints.length,
339 'Outgoing Transfer',
340 'Outgoing Transfers'
341 )}
342 </Grid.Item>
343 <Grid.Item
344 full
345 as={PointList}
346 points={outgoingPoints}
347 actions={(point, i) => (
348 <ActionButtons
349 actions={[
350 {
351 text: 'Cancel',
352 onClick: () => {
353 setPointCursor(Just(point));
354 // TODO: deep linking to fix this duplicate route
355 push(names.CANCEL_TRANSFER);
356 },
357 },
358 ]}
359 />
360 )}
361 inverted
362 />
363 </Grid.Item>
364 )}
366 {allPoints.length > 0 && (
367 <Grid.Item full as={Grid} gap={1}>
368 {/* <Grid.Item full as={H5}>
369 {pluralize(allPoints.length, 'ID')}
370 </Grid.Item> */}
371 <Grid.Item full as={PointList} points={allPoints} />
372 </Grid.Item>
373 )}
375 <Footer>
376 <Grid>
377 <Grid.Divider />
378 {isEclipticOwner && (
379 <>
380 <Grid.Item
381 full
382 as={ForwardButton}
383 detail="You have the authority to create a new Galaxy."
384 onClick={goCreateGalaxy}>
385 Create a galaxy
386 </Grid.Item>
387 <Grid.Divider />
388 </>
389 )}
390 {starReleasing && (
391 <>
392 <Grid.Item
393 full
394 as={ForwardButton}
395 detail="You have points being released"
396 onClick={goStarRelease}>
397 View Star Release
398 </Grid.Item>
399 <Grid.Divider />
400 </>
401 )}
402 </Grid>
403 </Footer>
404 </Grid>
405 </View>
406 );