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