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