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