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