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