on master help sign in
checkpoint: initial layout & implementation of multihome, pending click handlers and accept/reject logic
b1e0ff6Matt Condon (shrugs)
src/Bridge.js−1
@@ -28,7 +28,6 @@ const INITIAL_ROUTES = IS_STUBBED
28 { key: ROUTE_NAMES.LANDING },
28 { key: ROUTE_NAMES.LANDING },
29 { key: ROUTE_NAMES.LOGIN },
29 { key: ROUTE_NAMES.LOGIN },
30 { key: ROUTE_NAMES.POINTS },
30 { key: ROUTE_NAMES.POINTS },
31− { key: ROUTE_NAMES.POINT },
32 ]
31 ]
33 : hasDisclaimed()
32 : hasDisclaimed()
34 ? [{ key: ROUTE_NAMES.LANDING }]
33 ? [{ key: ROUTE_NAMES.LANDING }]
src/components/Passport.js+95−12
@@ -1,18 +1,23 @@
1−import React from 'react';
1+import React, { useMemo } from 'react';
2import cn from 'classnames';
2import cn from 'classnames';
3import { Just, Nothing } from 'folktale/maybe';
3import { Just, Nothing } from 'folktale/maybe';
4import * as ob from 'urbit-ob';
4import * as ob from 'urbit-ob';
5import { Grid, Flex, Text } from 'indigo-react';
5import { Grid, Flex, Text } from 'indigo-react';
6import { times } from 'lodash';
6import { times } from 'lodash';
7
7
8+import * as need from 'lib/need';
8import usePointBirthday from 'lib/usePointBirthday';
9import usePointBirthday from 'lib/usePointBirthday';
9
10
10−import {
11+import Blinky, {
11 LOADING_CHARACTER,
12 LOADING_CHARACTER,
12 INTERSTITIAL_CHARACTER,
13 INTERSTITIAL_CHARACTER,
13 matchBlinkyDate,
14 matchBlinkyDate,
14} from './Blinky';
15} from './Blinky';
16+import Sigil from './Sigil';
15import MaybeSigil from './MaybeSigil';
17import MaybeSigil from './MaybeSigil';
18+import usePermissionsForPoint from 'lib/usePermissionsForPoint';
19+import { useWallet } from 'store/wallet';
20+import { useSyncOwnedPoints } from 'lib/useSyncPoints';
16
21
17const TICKET_CHAR_LENGTH = 30;
22const TICKET_CHAR_LENGTH = 30;
18const TICKET = times(TICKET_CHAR_LENGTH, i =>
23const TICKET = times(TICKET_CHAR_LENGTH, i =>
@@ -25,18 +30,28 @@ const VISIBLE_ADDRESS_CHAR_COUNT = 20;
25const EMPTY_ADDRESS = times(VISIBLE_ADDRESS_CHAR_COUNT, () => '<').join('');
30const EMPTY_ADDRESS = times(VISIBLE_ADDRESS_CHAR_COUNT, () => '<').join('');
26const ADDRESS_BUFFER_CHARS = times(40, () => '<').join('');
31const ADDRESS_BUFFER_CHARS = times(40, () => '<').join('');
27
32
33+const INVERTED_COLORWAY = ['#000', '#fff', '#000'];
34+
35+const buildKeyType = permissions => {
36+ if (permissions.isOwner) {
37+ return 'Ownership';
38+ } else if (permissions.isManagementProxy) {
39+ return 'Management';
40+ } else if (permissions.isSpawnProxy) {
41+ return 'Spawn';
42+ } else if (permissions.isVotingProxy) {
43+ return 'Voting';
44+ } else if (permissions.isTransferProxy) {
45+ return 'Transfer';
46+ }
47+};
48+
28/**
49/**
29 * point is Maybe<number>
50 * point is Maybe<number>
30 * ticket is null | boolean
51 * ticket is null | boolean
31 * address is null | Maybe<string>
52 * address is null | Maybe<string>
32 */
53 */
33−export default function Passport({
34− point,
35− className,
36− ticket = null,
37− address = Nothing(),
54+function Passport({ point, className, ticket = null, address = Nothing() }) {
38− mini = false,
39−}) {
40 const birthday = usePointBirthday(point.getOrElse(null));
55 const birthday = usePointBirthday(point.getOrElse(null));
41 const visualBirthday = matchBlinkyDate(birthday);
56 const visualBirthday = matchBlinkyDate(birthday);
42
57
@@ -59,7 +74,7 @@ export default function Passport({
59
74
60 return (
75 return (
61 <Grid gap={4} className={cn('bg-black r8 p7', className)}>
76 <Grid gap={4} className={cn('bg-black r8 p7', className)}>
62− <Grid.Item cols={[1, 4]} className="bg-green">
77+ <Grid.Item cols={[1, 4]}>
63 <MaybeSigil patp={name} size={64} margin={0} />
78 <MaybeSigil patp={name} size={64} margin={0} />
64 </Grid.Item>
79 </Grid.Item>
65 <Grid.Item as={Flex} cols={[4, 13]} col justify="between">
80 <Grid.Item as={Flex} cols={[4, 13]} col justify="between">
@@ -68,12 +83,12 @@ export default function Passport({
68 <Flex.Item as={Text} className="mono f6 f5-ns gray4 uppercase">
83 <Flex.Item as={Text} className="mono f6 f5-ns gray4 uppercase">
69 Birth Time
84 Birth Time
70 </Flex.Item>
85 </Flex.Item>
71− <Flex.Item as={Text} className="mono f6 f5-ns gray4 ">
86+ <Flex.Item as={Text} className="mono f6 f5-ns gray4 uppercase">
72 {visualBirthday}
87 {visualBirthday}
73 </Flex.Item>
88 </Flex.Item>
74 </Flex.Item>
89 </Flex.Item>
75 </Grid.Item>
90 </Grid.Item>
76− {!mini && showTicket && (
91+ {showTicket && (
77 <Grid.Item full className="mv3" as={Flex} col>
92 <Grid.Item full className="mv3" as={Flex} col>
78 <Flex.Item as={Text} className="mono f6 f5-ns gray4 uppercase">
93 <Flex.Item as={Text} className="mono f6 f5-ns gray4 uppercase">
79 Master Ticket
94 Master Ticket
@@ -94,3 +109,71 @@ export default function Passport({
94 </Grid>
109 </Grid>
95 );
110 );
96}
111}
112+
113+// TODO: Passport.Mini should probably accept Maybe<> for point
114+// in order to display a pretty skeleton loading view
115+// but that requires making usePermissionsForPoint accept a Maybe<point>
116+// and I don't feel like that refactor is worth it right now, and the spec
117+// doesn't call for it, hence the TODO
118+Passport.Mini = function MiniPassport({ point, className, inverted, ...rest }) {
119+ useSyncOwnedPoints([point]);
120+
121+ const { wallet } = useWallet();
122+ const address = need.addressFromWallet(wallet);
123+
124+ const permissions = usePermissionsForPoint(address, point);
125+ const keyType = buildKeyType(permissions);
126+ const name = useMemo(() => ob.patp(point), [point]);
127+
128+ return (
129+ <Grid
130+ gap={4}
131+ className={cn(
132+ 'r8 p4',
133+ {
134+ 'bg-black': !inverted,
135+ 'bg-white b-gray4 b1': inverted,
136+ },
137+ className
138+ )}
139+ {...rest}>
140+ <Grid.Item cols={[1, 4]}>
141+ <Sigil
142+ patp={name}
143+ size={64}
144+ margin={0}
145+ colorway={inverted ? INVERTED_COLORWAY : undefined}
146+ />
147+ </Grid.Item>
148+ <Grid.Item as={Flex} cols={[4, 13]} col justify="between">
149+ <Flex.Item
150+ className={cn('mono f6 pb2', {
151+ white: !inverted,
152+ black: inverted,
153+ })}>
154+ {name}
155+ </Flex.Item>
156+ <Flex.Item as={Flex} col className={cn('mono f6 uppercase')}>
157+ <Flex.Item
158+ as={Text}
159+ className={cn({
160+ gray4: !inverted,
161+ black: inverted,
162+ })}>
163+ Key Type
164+ </Flex.Item>
165+ <Flex.Item
166+ as={Text}
167+ className={cn({
168+ white: !inverted,
169+ black: inverted,
170+ })}>
171+ {keyType || <Blinky />}
172+ </Flex.Item>
173+ </Flex.Item>
174+ </Grid.Item>
175+ </Grid>
176+ );
177+};
178+
179+export default Passport;
src/lib/usePermissionsForPoint.js+6−2
@@ -14,8 +14,10 @@ const NULL_PERMISSIONS = {
14 //
14 //
15 isOwner: false,
15 isOwner: false,
16 isActiveOwner: false,
16 isActiveOwner: false,
17− isTransferProxy: false,
18 isManagementProxy: false,
17 isManagementProxy: false,
18+ isSpawnProxy: false,
19+ isVotingProxy: false,
20+ isTransferProxy: false,
19 //
21 //
20 isManagementProxySet: false,
22 isManagementProxySet: false,
21 isSpawnProxySet: false,
23 isSpawnProxySet: false,
@@ -84,8 +86,10 @@ export default function usePermissionsForPoint(address, point) {
84 //
86 //
85 isOwner,
87 isOwner,
86 isActiveOwner,
88 isActiveOwner,
87− isTransferProxy,
88 isManagementProxy,
89 isManagementProxy,
90+ isSpawnProxy,
91+ isVotingProxy,
92+ isTransferProxy,
89 //
93 //
90 isManagementProxySet,
94 isManagementProxySet,
91 isSpawnProxySet,
95 isSpawnProxySet,
src/lib/wallet.js+3
@@ -102,3 +102,6 @@ export const walletFromMnemonic = (mnemonic, hdpath, passphrase) => {
102 const wallet = seed.chain(sd => toWallet(sd, hdpath));
102 const wallet = seed.chain(sd => toWallet(sd, hdpath));
103 return wallet;
103 return wallet;
104};
104};
105+
106+export const abbreviateAddress = address =>
107+ `${address.slice(0, 4)}…${address.slice(-4)}`;
src/store/lib/useControlledPointsStore.js+40−39
@@ -1,29 +1,15 @@
1−import { useState, useCallback } from 'react';
1+import { useState, useEffect, useCallback } from 'react';
2import { Just, Nothing } from 'folktale/maybe';
2import { Just, Nothing } from 'folktale/maybe';
3+import Result from 'folktale/result';
3import * as azimuth from 'azimuth-js';
4import * as azimuth from 'azimuth-js';
4
5
5import { useNetwork } from '../network';
6import { useNetwork } from '../network';
6import { useWallet } from 'store/wallet';
7import { useWallet } from 'store/wallet';
7
8
8−const EMPTY_CONTROLLED_POINTS = {
9− ownedPoints: Nothing(),
10− incomingPoints: Nothing(),
11− managingPoints: Nothing(),
12− votingPoints: Nothing(),
13− spawningPoints: Nothing(),
14−};
15−
16export default function useControlledPointsStore() {
9export default function useControlledPointsStore() {
17 const { contracts } = useNetwork();
10 const { contracts } = useNetwork();
18 const { wallet } = useWallet();
11 const { wallet } = useWallet();
19− const [controlledPointsCache, _setControlledPointsCache] = useState(
12+ const [controlledPoints, _setControlledPoints] = useState(Nothing());
20− EMPTY_CONTROLLED_POINTS
21− );
22−
23− const getControlledPoints = useCallback(
24− () => controlledPointsCache || EMPTY_CONTROLLED_POINTS,
25− [controlledPointsCache]
26− );
27
13
28 const syncControlledPoints = useCallback(async () => {
14 const syncControlledPoints = useCallback(async () => {
29 const _contracts = contracts.getOrElse(null);
15 const _contracts = contracts.getOrElse(null);
@@ -32,33 +18,48 @@ export default function useControlledPointsStore() {
32 return;
18 return;
33 }
19 }
34
20
21+ _setControlledPoints(Nothing());
22+
35 const address = _wallet.address;
23 const address = _wallet.address;
36
24
25+ try {
37− const [
26+ const [
38− ownedPoints,
27+ ownedPoints,
39− incomingPoints,
28+ incomingPoints,
40− managingPoints,
29+ managingPoints,
41− votingPoints,
30+ votingPoints,
42− spawningPoints,
31+ spawningPoints,
43− ] = await Promise.all([
32+ ] = await Promise.all([
44− azimuth.azimuth.getOwnedPoints(_contracts, address),
33+ azimuth.azimuth.getOwnedPoints(_contracts, address),
45− azimuth.azimuth.getTransferringFor(_contracts, address),
34+ azimuth.azimuth.getTransferringFor(_contracts, address),
46− azimuth.azimuth.getManagerFor(_contracts, address),
35+ azimuth.azimuth.getManagerFor(_contracts, address),
47− azimuth.azimuth.getVotingFor(_contracts, address),
36+ azimuth.azimuth.getVotingFor(_contracts, address),
48− azimuth.azimuth.getSpawningFor(_contracts, address),
37+ azimuth.azimuth.getSpawningFor(_contracts, address),
49− ]);
38+ ]);
39+
40+ _setControlledPoints(
41+ Just(
42+ Result.Ok({
43+ ownedPoints,
44+ incomingPoints,
45+ managingPoints,
46+ votingPoints,
47+ spawningPoints,
48+ })
49+ )
50+ );
51+ } catch (error) {
52+ _setControlledPoints(Just(Result.Error(error)));
53+ }
54+ }, [contracts, wallet]);
50
55
51− _setControlledPointsCache({
56+ // sync controlled points whenever wallet or contracts changes
52− ownedPoints: Just(ownedPoints),
57+ useEffect(() => {
53− incomingPoints: Just(incomingPoints),
58+ syncControlledPoints();
54− managingPoints: Just(managingPoints),
59+ }, [syncControlledPoints]);
55− votingPoints: Just(votingPoints),
56− spawningPoints: Just(spawningPoints),
57− });
58− }, [contracts, wallet, _setControlledPointsCache]);
59
60
60 return {
61 return {
61− getControlledPoints,
62+ controlledPoints,
62 syncControlledPoints,
63 syncControlledPoints,
63 };
64 };
64}
65}
src/views/Points.js+169−148
@@ -1,186 +1,207 @@
1import React from 'react';
1import React from 'react';
2import { Just, Nothing } from 'folktale/maybe';
2import { Just, Nothing } from 'folktale/maybe';
3−import { H1, H2, P, Grid } from 'indigo-react';
3+import { Grid, H5, H1, HelpText, LinkButton, Flex } from 'indigo-react';
4−
5−import { ROUTE_NAMES } from 'lib/routeNames';
6−import { isZeroAddress } from 'lib/wallet';
7−import useIsEclipticOwner from 'lib/useIsEclipticOwner';
8−import { useSyncKnownPoints } from 'lib/useSyncPoints';
9
4
10import { useHistory } from 'store/history';
5import { useHistory } from 'store/history';
11import { useWallet } from 'store/wallet';
6import { useWallet } from 'store/wallet';
12import { usePointCache } from 'store/pointCache';
7import { usePointCache } from 'store/pointCache';
8+import { usePointCursor } from 'store/pointCursor';
9+
10+import * as need from 'lib/need';
11+import { ROUTE_NAMES } from 'lib/routeNames';
12+import { isZeroAddress, abbreviateAddress } from 'lib/wallet';
13+import useIsEclipticOwner from 'lib/useIsEclipticOwner';
14+import { useSyncKnownPoints } from 'lib/useSyncPoints';
13
15
14import View from 'components/View';
16import View from 'components/View';
15−import { ForwardButton } from 'components/Buttons';
17+import FooterButton from 'components/FooterButton';
16−import useLifecycle from 'lib/useLifecycle';
18+import Blinky from 'components/Blinky';
19+import Passport from 'components/Passport';
20+
21+const getFromMaybe = (obj, key, defaultValue) =>
22+ obj.matchWith({
23+ Nothing: () => defaultValue,
24+ Just: p =>
25+ p.value.matchWith({
26+ Ok: r => r.value[key],
27+ Error: e => defaultValue,
28+ }),
29+ });
17
30
18−//TODO new component
31+const hasTransferProxy = (details, point) =>
19−import { usePointCursor } from 'store/pointCursor';
32+ !isZeroAddress(details.transferProxy);
20−import * as ob from 'urbit-ob';
33+
21−const PointList = function(props) {
34+const PointList = function({ points, className, actions, ...rest }) {
22 const { setPointCursor } = usePointCursor();
35 const { setPointCursor } = usePointCursor();
23 const history = useHistory();
36 const history = useHistory();
24− const { points, loading } = props;
37+
25−
26− return points.length === 0 ? (
38+ return (
27− <p>{loading ? 'Loading...' : 'No points to display'}</p>
39+ <Grid gap={3} className={className}>
28− ) : (
29− <>
30− {points.map(point => (
40+ {points.map((point, i) => (
31− <P
32− key={point}
41+ <Grid.Item half={(i % 2) + 1} key={point}>
42+ <Flex col>
43+ <Passport.Mini
44+ point={point}
45+ className="clickable"
33− onClick={() => {
46+ onClick={() => {
34− setPointCursor(Just(point));
47+ setPointCursor(Just(point));
35− history.push(ROUTE_NAMES.POINT);
48+ history.push(history.names.POINT);
36− }}>
49+ }}
37− {ob.patp(point)}
50+ {...rest}
38− </P>
51+ />
52+ <Flex.Item className="mt2">{actions}</Flex.Item>
53+ </Flex>
54+ </Grid.Item>
39 ))}
55 ))}
40− </>
56+ </Grid>
41 );
57 );
42};
58};
43
59
44−const matchArray = obj =>
60+function ActionButtons({ actions = [] }) {
45− obj.matchWith({ Nothing: () => [], Just: p => p.value });
61+ return (
46−
62+ <Flex row>
47−const hasTransferProxy = (details, point) =>
63+ {actions.map(action => (
48− !isZeroAddress(details.transferProxy);
64+ <Flex.Item
65+ key={action.text}
66+ as={LinkButton}
67+ className="mr3"
68+ onClick={action.onClick}>
69+ {action.text}
70+ </Flex.Item>
71+ ))}
72+ </Flex>
73+ );
74+}
49
75
50export default function Points() {
76export default function Points() {
51 const { wallet } = useWallet();
77 const { wallet } = useWallet();
52 const history = useHistory();
78 const history = useHistory();
53− const {
54− getControlledPoints,
55− syncControlledPoints,
56− getDetails,
57− } = usePointCache();
79+ const { controlledPoints, getDetails } = usePointCache();
58− const {
59− ownedPoints,
60− incomingPoints,
61− managingPoints,
62− votingPoints,
63− spawningPoints,
64− } = getControlledPoints(wallet);
65 const isEclipticOwner = useIsEclipticOwner();
80 const isEclipticOwner = useIsEclipticOwner();
66
81
67− const owned = matchArray(ownedPoints);
82+ const address = need.addressFromWallet(wallet);
68− const incoming = matchArray(incomingPoints);
69− const managing = matchArray(managingPoints);
70− const voting = matchArray(votingPoints);
71− const spawning = matchArray(spawningPoints);
72
83
73− // sync controlled points on mount
84+ const loading = Nothing.hasInstance(controlledPoints);
74− useLifecycle(() => {
75− syncControlledPoints();
76− });
77
85
78− // sync display details for known points
86+ const ownedPoints = getFromMaybe(controlledPoints, 'ownedPoints', []);
79− useSyncKnownPoints([
87+ const incomingPoints = getFromMaybe(controlledPoints, 'incomingPoints', []);
80− ...owned,
88+ const managingPoints = getFromMaybe(controlledPoints, 'managingPoints', []);
81− ...incoming,
89+ const votingPoints = getFromMaybe(controlledPoints, 'votingPoints', []);
82− ...managing,
90+ const spawningPoints = getFromMaybe(controlledPoints, 'spawningPoints', []);
83− ...voting,
84− ...spawning,
85− ]);
86−
87− const loading = Nothing.hasInstance(ownedPoints);
88−
89− const outgoing = ownedPoints.filter(point =>
91+ const outgoingPoints = ownedPoints.filter(point =>
90 getDetails(point).matchWith({
92 getDetails(point).matchWith({
91 Nothing: () => false,
93 Nothing: () => false,
92 Just: p => hasTransferProxy(p.value),
94 Just: p => hasTransferProxy(p.value),
93 })
95 })
94 );
96 );
95
97
98+ const multipassPoints = [
99+ ...ownedPoints,
100+ ...managingPoints,
101+ ...votingPoints,
102+ ...spawningPoints,
103+ ];
104+
105+ // sync display details for known points
106+ useSyncKnownPoints([
107+ ...ownedPoints,
108+ ...incomingPoints,
109+ ...managingPoints,
110+ ...votingPoints,
111+ ...spawningPoints,
112+ ]);
113+
96 return (
114 return (
97 <View>
115 <View>
98− <H1>Points</H1>
99−
100− <P>
101− A point is an identity on the Ethereum blockchain. Points declare keys
102− for ships on the Arvo network.
103− </P>
104−
105− <Grid>
116+ <Grid className="mt6">
106− <Grid.Divider />
107− <Grid.Item full>
117+ <Grid.Item full as={H1} className="f6 mono gray4 mb4">
108− <ForwardButton
118+ {abbreviateAddress(address)}
109− detail="View a point on Azimuth"
110− onClick={() => history.push(ROUTE_NAMES.VIEW_POINT)}>
111− View a point
112− </ForwardButton>
113 </Grid.Item>
119 </Grid.Item>
114− <Grid.Divider />
120+
115− {isEclipticOwner && (
121+ {loading && (
116− <>
122+ <Grid.Item full as={HelpText} className="mt8 t-center">
117− <Grid.Item full>
123+ <Blinky /> Loading...
118− <ForwardButton
124+ </Grid.Item>
119− detail="You have the authority to create a new Galaxy."
125+ )}
120− onClick={() => history.push(ROUTE_NAMES.CREATE_GALAXY)}>
126+
121− Create a galaxy
127+ {incomingPoints.length > 0 && (
122− </ForwardButton>
128+ <Grid.Item full as={Grid} gap={1} className="mb6">
129+ <Grid.Item full as={H5}>
130+ Incoming Transfers
123 </Grid.Item>
131 </Grid.Item>
124− <Grid.Divider />
132+ <Grid.Item
125− </>
133+ full
134+ as={PointList}
135+ points={incomingPoints}
136+ actions={
137+ <ActionButtons
138+ actions={[
139+ {
140+ text: 'Accept',
141+ onClick: () => {
142+ // TODO: accept
143+ console.log('accept');
144+ },
145+ },
146+ {
147+ text: 'Reject',
148+ onClick: () => {
149+ // TODO: reject
150+ console.log('reject');
151+ },
152+ },
153+ ]}
154+ />
155+ }
156+ inverted
157+ />
158+ </Grid.Item>
159+ )}
160+
161+ {outgoingPoints.length > 0 && (
162+ <Grid.Item full as={Grid} gap={1} className="mb6">
163+ <Grid.Item full as={H5}>
164+ Outgoing Transfers
165+ </Grid.Item>
166+ <Grid.Item
167+ full
168+ as={PointList}
169+ points={outgoingPoints}
170+ actions={
171+ <ActionButtons
172+ actions={[
173+ {
174+ text: 'Cancel',
175+ onClick: () => {
176+ // TODO: cancel
177+ console.log('cancel');
178+ },
179+ },
180+ ]}
181+ />
182+ }
183+ inverted
184+ />
185+ </Grid.Item>
126 )}
186 )}
127− </Grid>
128
187
129− {incoming.length > 0 && (
188+ {multipassPoints.length > 0 && (
130− <>
189+ <Grid.Item full as={Grid} gap={1}>
131− <H2>Incoming Transfers</H2>
190+ <Grid.Item full as={H5}>
132− <P>
191+ Multipasses
133− You do not own these points until you accept the incoming transfer.
192+ </Grid.Item>
134− You may reject any incoming transfers.
193+ <Grid.Item full as={PointList} points={multipassPoints} />
135− </P>
194+ </Grid.Item>
136− <PointList routeHandler={history.push} points={incoming} />
137− </>
138− )}
139−
140− {outgoing.length > 0 && (
141− <>
142− <H2>Outgoing Transfers</H2>
143− <P>
144− You own these points until the recipient accepts the incoming
145− transfer. You may cancel the transfer until accepted.
146− </P>
147− <PointList routeHandler={history.push} points={outgoing} />
148− </>
149− )}
150−
151− <H2>Your Points</H2>
152−
153− <PointList routeHandler={history.push} points={owned} loading={loading} />
154−
155− {managing.length > 0 && (
156− <>
157− <H2>You Are a Management Proxy For</H2>
158− <P>
159− You can configure or set network keys and conduct sponsorship
160− related operations for these points.
161− </P>
162− <PointList routeHandler={history.push} points={managing} />
163− </>
164− )}
195+ )}
165−
196+
166− {voting.length > 0 && (
197+ {isEclipticOwner && (
167− <>
198+ <FooterButton
168− <H2>You Are a Voting Proxy For</H2>
199+ detail="You have the authority to create a new Galaxy."
169− <P>
200+ onClick={() => history.push(ROUTE_NAMES.CREATE_GALAXY)}>
170− Since you are part of the Galactic Senate, you can cast votes on new
201+ Create a galaxy
171− Azimuth proposals on behalf of these points.
202+ </FooterButton>
172− </P>
203+ )}
173− <PointList routeHandler={history.push} points={voting} />
204+ </Grid>
174− </>
175− )}
176−
177− {spawning.length > 0 && (
178− <>
179− <H2>You Are a Spawn Proxy For</H2>
180− <P>You can create new child points under these points.</P>
181− <PointList routeHandler={history.push} points={spawning} />
182− </>
183− )}
184 </View>
205 </View>
185 );
206 );
186}
207}