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