on master help sign in
show actual point in invite list view
4db9a8eLiam Fitzgerald
src/components/Tabs.js+3−1
@@ -8,6 +8,7 @@ export default function Tabs({
8 currentTab,
8 currentTab,
9 onTabChange,
9 onTabChange,
10 className,
10 className,
11+ tabClassName,
11 ...rest
12 ...rest
12}) {
13}) {
13 const Tab = views[currentTab];
14 const Tab = views[currentTab];
@@ -15,7 +16,7 @@ export default function Tabs({
15 return (
16 return (
16 <Grid className={className}>
17 <Grid className={className}>
17 <Grid.Item full as={Flex} className="b-gray3 bb1 scroll-x hidden-y">
18 <Grid.Item full as={Flex} className="b-gray3 bb1 scroll-x hidden-y">
18− <Flex.Item as={Flex}>
19+ <Flex.Item as={Flex} className="flex1">
19 {options.map((option, i) => {
20 {options.map((option, i) => {
20 const isActive = option.value === currentTab;
21 const isActive = option.value === currentTab;
21 const isFirst = i === 0;
22 const isFirst = i === 0;
@@ -25,6 +26,7 @@ export default function Tabs({
25 key={option.value}
26 key={option.value}
26 onClick={() => onTabChange(option.value)}
27 onClick={() => onTabChange(option.value)}
27 className={cn(
28 className={cn(
29+ tabClassName,
28 'f5 pv3 pointer nowrap',
30 'f5 pv3 pointer nowrap',
29 {
31 {
30 'black b-black bb1': isActive,
32 'black b-black bb1': isActive,
src/components/View.js+1−1
@@ -53,7 +53,7 @@ function View({
53 <Flex.Item className={insetPadding} as={NavHeader.Target} />
53 <Flex.Item className={insetPadding} as={NavHeader.Target} />
54 );
54 );
55 },
55 },
56− [insetPadding, showBackButton]
56+ [goLogout, insetPadding, showBackButton]
57 );
57 );
58
58
59 return (
59 return (
src/form/EmailChipInput.js+31−13
@@ -1,22 +1,27 @@
1import React, { useState, useCallback } from 'react';
1import React, { useState, useCallback } from 'react';
2+import { isEqual } from 'lodash';
2import { Flex, ErrorText } from 'indigo-react';
3import { Flex, ErrorText } from 'indigo-react';
3import cn from 'classnames';
4import cn from 'classnames';
4import { useField } from 'react-final-form';
5import { useField } from 'react-final-form';
5
6
6import { hasErrors } from 'form/validators';
7import { hasErrors } from 'form/validators';
7
8
8−const Chip = ({ value, onDelete }) => {
9+const Chip = ({ value, onDelete, disabled }) => {
9 return (
10 return (
11+ <Flex
10− <Flex className="r32 ph2 m1 flex-center h7 bg-gray2">
12+ className={cn('r32 ph2 m1 flex-center h7', {
13+ 'bg-gray2': !disabled,
14+ 'bg-gray1 gray4': disabled,
15+ })}>
11 <Flex.Item>{value}</Flex.Item>
16 <Flex.Item>{value}</Flex.Item>
12− <Flex.Item onClick={onDelete} className="ml1 f6">
17+ <Flex.Item onClick={disabled ? onDelete : undefined} className="ml1 f6">
13 ✗
18 ✗
14 </Flex.Item>
19 </Flex.Item>
15 </Flex>
20 </Flex>
16 );
21 );
17};
22};
18
23
19−const EmailChipInput = ({ className, name, label }) => {
24+const EmailChipInput = ({ className, name, label, disabled }) => {
20 const [chips, _setChips] = useState([]);
25 const [chips, _setChips] = useState([]);
21
26
22 const [value, setValue] = useState('');
27 const [value, setValue] = useState('');
@@ -30,13 +35,20 @@ const EmailChipInput = ({ className, name, label }) => {
30
35
31 const {
36 const {
32 input,
37 input,
33− meta: { active, error },
38+ meta: { active, error, dirtySinceLastSubmit },
34 } = useField(name, {
39 } = useField(name, {
35− subscription: { active: true, error: true },
40+ subscription: {
41+ active: true,
42+ error: true,
43+ value: true,
44+ dirtySinceLastSubmit: true,
45+ },
46+ isEqual,
36 });
47 });
37
48
38 const setChips = useCallback(
49 const setChips = useCallback(
39 chips => {
50 chips => {
51+ console.log(chips);
40 _setChips(chips);
52 _setChips(chips);
41 input.onChange(chips);
53 input.onChange(chips);
42 },
54 },
@@ -56,9 +68,11 @@ const EmailChipInput = ({ className, name, label }) => {
56 } else if ('Backspace' === event.key && value === '') {
68 } else if ('Backspace' === event.key && value === '') {
57 event.preventDefault();
69 event.preventDefault();
58 setChips(chips.slice(0, -1));
70 setChips(chips.slice(0, -1));
71+ } else if (disabled) {
72+ event.preventDefault();
59 }
73 }
60 },
74 },
61− [setChips, chips, value]
75+ [setChips, chips, value, addToChips, disabled]
62 );
76 );
63
77
64 const handleDelete = useCallback(
78 const handleDelete = useCallback(
@@ -72,10 +86,10 @@ const EmailChipInput = ({ className, name, label }) => {
72 e => {
86 e => {
73 if (value !== '') {
87 if (value !== '') {
74 addToChips();
88 addToChips();
75− input.handleBlur();
89+ input.onBlur();
76 }
90 }
77 },
91 },
78− [addToChips, value]
92+ [addToChips, value, input]
79 );
93 );
80
94
81 const hasError = hasErrors(error);
95 const hasError = hasErrors(error);
@@ -89,11 +103,16 @@ const EmailChipInput = ({ className, name, label }) => {
89 )}
103 )}
90 <Flex.Item
104 <Flex.Item
91 className={cn('flex b1 flex-wrap', {
105 className={cn('flex b1 flex-wrap', {
92− 'b-green2': active && !hasError,
106+ 'b-green2': active && !disabled && !hasError,
93− 'b-red3': active && hasError,
107+ 'b-red3': active && !disabled && hasError,
94 })}>
108 })}>
95 {chips.map((chip, idx) => (
109 {chips.map((chip, idx) => (
110+ <Chip
111+ key={idx}
112+ value={chip}
113+ disabled={disabled}
96− <Chip key={idx} value={chip} onDelete={handleDelete(idx)} />
114+ onDelete={handleDelete(idx)}
115+ />
97 ))}
116 ))}
98
117
99 <input
118 <input
@@ -104,7 +123,6 @@ const EmailChipInput = ({ className, name, label }) => {
104 name={name}
123 name={name}
105 onChange={handleChange}
124 onChange={handleChange}
106 onKeyDown={handleKeyDown}
125 onKeyDown={handleKeyDown}
107− onFocus={input.onFocus}
108 onBlur={handleBlur}
126 onBlur={handleBlur}
109 />
127 />
110 </Flex.Item>
128 </Flex.Item>
src/form/validators.js+3
@@ -15,6 +15,7 @@ import {
15 validateNotNullAddress,
15 validateNotNullAddress,
16 validateHexPrefix,
16 validateHexPrefix,
17 validateLength,
17 validateLength,
18+ validateUnique,
18} from 'lib/validators';
19} from 'lib/validators';
19import isPromise from 'lib/isPromise';
20import isPromise from 'lib/isPromise';
20
21
@@ -105,6 +106,8 @@ export const buildNumberValidator = (min = 0) =>
105export const buildEmailValidator = validate =>
106export const buildEmailValidator = validate =>
106 buildValidator([validateNotEmpty, validateEmail], validate);
107 buildValidator([validateNotEmpty, validateEmail], validate);
107
108
109+export const buildEmailArrayValidator = () =>
110+ buildValidator([validateUnique, buildArrayValidator(buildEmailValidator())]);
108// the form validator is the composition of all of the field validators
111// the form validator is the composition of all of the field validators
109// plus an additional form validator function
112// plus an additional form validator function
110export const composeValidator = (
113export const composeValidator = (
src/lib/validators.js+5
@@ -102,3 +102,8 @@ export const validateNotNullAddress = validateNotAny([
102 ETH_ZERO_ADDR,
102 ETH_ZERO_ADDR,
103 ETH_ZERO_ADDR_SHORT,
103 ETH_ZERO_ADDR_SHORT,
104]);
104]);
105+
106+export const validateUnique = arr => {
107+ const res = [...new Set(arr)].length !== arr.length && 'Must be unique';
108+ return res;
109+};
src/views/Invite/InviteEmail.js+9−11
@@ -142,15 +142,12 @@ export default function InviteEmail() {
142 const isFailed = status === STATUS.FAILURE;
142 const isFailed = status === STATUS.FAILURE;
143 const isDone = status === STATUS.SUCCESS;
143 const isDone = status === STATUS.SUCCESS;
144
144
145− const validateHasReceived = useCallback(
145+ const validateHasReceived = useCallback(async email => {
146− async email => {
147− const hasReceived = await getHasReceived(email);
146+ // const hasReceived = await getHasReceived(email);
148− if (hasReceived) {
147+ // if (hasReceived) {
149− return HAS_RECEIVED_TEXT;
148+ // return HAS_RECEIVED_TEXT;
150− }
149+ // }
151− },
150+ }, []);
152− [getHasReceived]
153− );
154
151
155 const validateForm = useCallback((values, errors) => {
152 const validateForm = useCallback((values, errors) => {
156 if (hasErrors(errors)) {
153 if (hasErrors(errors)) {
@@ -384,12 +381,13 @@ export default function InviteEmail() {
384 }, [
381 }, [
385 web3,
382 web3,
386 wallet,
383 wallet,
387− point,
388 gasPrice,
384 gasPrice,
389− invites,
385+ point,
390 clearReceipts,
386 clearReceipts,
387+ invites,
391 addReceipt,
388 addReceipt,
392 sendMail,
389 sendMail,
390+ message,
393 ]);
391 ]);
394
392
395 const onSubmit = useCallback(
393 const onSubmit = useCallback(
src/views/Point.js+113−3
@@ -1,7 +1,10 @@
1−import React, { useCallback } from 'react';
1+import React, { useCallback, useState } from 'react';
2−import { Just } from 'folktale/maybe';
2+import { Just, Nothing } from 'folktale/maybe';
3import { Grid } from 'indigo-react';
3import { Grid } from 'indigo-react';
4import { azimuth } from 'azimuth-js';
4import { azimuth } from 'azimuth-js';
5+import cn from 'classnames';
6+import { take } from 'lodash';
7+import ob from 'urbit-ob';
5
8
6import { usePointCursor } from 'store/pointCursor';
9import { usePointCursor } from 'store/pointCursor';
7import { useWallet } from 'store/wallet';
10import { useWallet } from 'store/wallet';
@@ -13,6 +16,8 @@ import { ForwardButton, BootUrbitOSButton } from 'components/Buttons';
13import CopyButton from 'components/CopyButton';
16import CopyButton from 'components/CopyButton';
14import { matchBlinky } from 'components/Blinky';
17import { matchBlinky } from 'components/Blinky';
15import DownloadSigilButton from 'components/DownloadSigilButton';
18import DownloadSigilButton from 'components/DownloadSigilButton';
19+import BarGraph from 'components/BarGraph';
20+import MaybeSigil from 'components/MaybeSigil';
16
21
17import * as need from 'lib/need';
22import * as need from 'lib/need';
18import useInvites from 'lib/useInvites';
23import useInvites from 'lib/useInvites';
@@ -21,6 +26,46 @@ import useCurrentPermissions from 'lib/useCurrentPermissions';
21import { useLocalRouter } from 'lib/LocalRouter';
26import { useLocalRouter } from 'lib/LocalRouter';
22import useKeyfileGenerator from 'lib/useKeyfileGenerator';
27import useKeyfileGenerator from 'lib/useKeyfileGenerator';
23
28
29+import InviteCohort from 'views/Invite/Cohort';
30+
31+const InviteSigilList = ({ className, pendingPoints, acceptedPoints }) => {
32+ const _acceptedPoints = take(
33+ acceptedPoints.getOrElse([]).map(x => Just(ob.patp(x))),
34+ 6
35+ );
36+
37+ const _pendingPoints = take(
38+ pendingPoints.getOrElse([]).map(x => Just(ob.patp(x))),
39+ 6 - _acceptedPoints.length
40+ );
41+
42+ const empty = [
43+ ...Array(
44+ Math.max(6 - _acceptedPoints.length - _pendingPoints.length, 0)
45+ ).keys(),
46+ ].map(() => Nothing());
47+
48+ const renderSigil = (points, colors, klassName) => {
49+ return (
50+ <>
51+ {points.map((point, idx) => (
52+ <div className={cn(klassName, 'h9 w9')}>
53+ <MaybeSigil patp={point} size={50} colors={colors} />
54+ </div>
55+ ))}
56+ </>
57+ );
58+ };
59+
60+ return (
61+ <div className={cn('flex justify-between', className)}>
62+ {renderSigil(_acceptedPoints, ['#000000', '#FFFFFF'])}
63+ {renderSigil(_pendingPoints, ['#ee892b', '#FFFFFF'])}
64+ {renderSigil(empty, [], 'b1 b-black')}
65+ </div>
66+ );
67+};
68+
24export default function Point() {
69export default function Point() {
25 const { pop, push, names } = useLocalRouter();
70 const { pop, push, names } = useLocalRouter();
26 const { pointCursor } = usePointCursor();
71 const { pointCursor } = usePointCursor();
@@ -38,7 +83,22 @@ export default function Point() {
38 } = useCurrentPermissions();
83 } = useCurrentPermissions();
39
84
40 // fetch the invites for the current cursor
85 // fetch the invites for the current cursor
86+ const invites = useInvites(point);
87+ const {
41− const { availableInvites } = useInvites(point);
88+ availableInvites,
89+ sentInvites,
90+ acceptedInvites,
91+ pendingPoints,
92+ acceptedPoints,
93+ } = invites;
94+
95+ const showInvites = !(
96+ acceptedInvites.getOrElse(0) === 0 && sentInvites.getOrElse(0) === 0
97+ );
98+
99+ const hasInvites = showInvites || availableInvites.getOrElse(0) !== 0;
100+ //&&
101+ // availableInvites.getOrElse(0) === 0
42
102
43 const goAdmin = useCallback(() => push(names.ADMIN), [push, names]);
103 const goAdmin = useCallback(() => push(names.ADMIN), [push, names]);
44
104
@@ -54,6 +114,10 @@ export default function Point() {
54 push,
114 push,
55 ]);
115 ]);
56
116
117+ const isPlanet = azimuth.getPointSize(point) === azimuth.PointSize.Planet;
118+
119+ const [showInviteForm, setShowInviteForm] = useState(false);
120+
57 const inviteButton = (() => {
121 const inviteButton = (() => {
58 switch (azimuth.getPointSize(point)) {
122 switch (azimuth.getPointSize(point)) {
59 case azimuth.PointSize.Planet:
123 case azimuth.PointSize.Planet:
@@ -103,6 +167,52 @@ export default function Point() {
103 address={Just(address)}
167 address={Just(address)}
104 animationMode={'slide'}
168 animationMode={'slide'}
105 />
169 />
170+ <Grid gap={3}>
171+ {isPlanet && hasInvites && (
172+ <>
173+ <Grid.Item full>
174+ Invite Group
175+ <br />
176+ </Grid.Item>
177+ {showInvites && (
178+ <>
179+ <Grid.Item
180+ full
181+ as={BarGraph}
182+ available={availableInvites}
183+ sent={sentInvites}
184+ accepted={acceptedInvites}
185+ />
186+ <Grid.Item
187+ full
188+ as={InviteSigilList}
189+ pendingPoints={pendingPoints}
190+ acceptedPoints={acceptedPoints}
191+ />
192+ </>
193+ )}
194+ {!showInvites && (
195+ <>
196+ <Grid.Item full className="b-gray4 b-dotted b1 self-center">
197+ <div className="p4 pv8 t-center gray4">
198+ Start your invite group by adding members
199+ </div>
200+ </Grid.Item>
201+ </>
202+ )}
203+ {!showInviteForm && availableInvites.getOrElse(0) > 0 && (
204+ <Grid.Item
205+ full
206+ solid
207+ as={ForwardButton}
208+ onClick={() => setShowInviteForm(true)}>
209+ Add Members
210+ </Grid.Item>
211+ )}
212+ {showInviteForm && <InviteCohort />}
213+ </>
214+ )}
215+ </Grid>
106 <Grid className="pt2">
216 <Grid className="pt2">
107 {inviteButton}
217 {inviteButton}
108 <Grid.Item
218 <Grid.Item