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