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