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