on master help sign in
show actual point in invite list view
commit4db9a8ef0b03a3c02b87cc149120172081953e22 authorLiam Fitzgerald <liam@urizen.co> dateWed 1 Jan 2020, 10:24 (7 years ago) parent0000e51
7 files changed+165−29
src/components/Tabs.js+3−1 src/components/View.js+1−1 src/form/EmailChipInput.js+31−13 src/form/validators.js+3 src/lib/validators.js+5 src/views/Invite/InviteEmail.js+9−11 src/views/Point.js+113−3
src/components/Tabs.js+3−1
@@ -8,6 +8,7 @@ export default function Tabs({
88 currentTab,
99 onTabChange,
1010 className,
11+ tabClassName,
1112 ...rest
1213}) {
1314 const Tab = views[currentTab];
@@ -15,7 +16,7 @@ export default function Tabs({
1516 return (
1617 <Grid className={className}>
1718 <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">
1920 {options.map((option, i) => {
2021 const isActive = option.value === currentTab;
2122 const isFirst = i === 0;
@@ -25,6 +26,7 @@ export default function Tabs({
2526 key={option.value}
2627 onClick={() => onTabChange(option.value)}
2728 className={cn(
29+ tabClassName,
2830 'f5 pv3 pointer nowrap',
2931 {
3032 'black b-black bb1': isActive,
src/components/View.js+1−1
@@ -53,7 +53,7 @@ function View({
5353 <Flex.Item className={insetPadding} as={NavHeader.Target} />
5454 );
5555 },
56− [insetPadding, showBackButton]
56+ [goLogout, insetPadding, showBackButton]
5757 );
5858
5959 return (
src/form/EmailChipInput.js+31−13
@@ -1,22 +1,27 @@
11import React, { useState, useCallback } from 'react';
2+import { isEqual } from 'lodash';
23import { Flex, ErrorText } from 'indigo-react';
34import cn from 'classnames';
45import { useField } from 'react-final-form';
56
67import { hasErrors } from 'form/validators';
78
8−const Chip = ({ value, onDelete }) => {
9+const Chip = ({ value, onDelete, disabled }) => {
910 return (
10− <Flex className="r32 ph2 m1 flex-center h7 bg-gray2">
11+ <Flex
12+ className={cn('r32 ph2 m1 flex-center h7', {
13+ 'bg-gray2': !disabled,
14+ 'bg-gray1 gray4': disabled,
15+ })}>
1116 <Flex.Item>{value}</Flex.Item>
12− <Flex.Item onClick={onDelete} className="ml1 f6">
17+ <Flex.Item onClick={disabled ? onDelete : undefined} className="ml1 f6">
1318 ✗
1419 </Flex.Item>
1520 </Flex>
1621 );
1722};
1823
19−const EmailChipInput = ({ className, name, label }) => {
24+const EmailChipInput = ({ className, name, label, disabled }) => {
2025 const [chips, _setChips] = useState([]);
2126
2227 const [value, setValue] = useState('');
@@ -30,13 +35,20 @@ const EmailChipInput = ({ className, name, label }) => {
3035
3136 const {
3237 input,
33− meta: { active, error },
38+ meta: { active, error, dirtySinceLastSubmit },
3439 } = 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,
3647 });
3748
3849 const setChips = useCallback(
3950 chips => {
51+ console.log(chips);
4052 _setChips(chips);
4153 input.onChange(chips);
4254 },
@@ -56,9 +68,11 @@ const EmailChipInput = ({ className, name, label }) => {
5668 } else if ('Backspace' === event.key && value === '') {
5769 event.preventDefault();
5870 setChips(chips.slice(0, -1));
71+ } else if (disabled) {
72+ event.preventDefault();
5973 }
6074 },
61− [setChips, chips, value]
75+ [setChips, chips, value, addToChips, disabled]
6276 );
6377
6478 const handleDelete = useCallback(
@@ -72,10 +86,10 @@ const EmailChipInput = ({ className, name, label }) => {
7286 e => {
7387 if (value !== '') {
7488 addToChips();
75− input.handleBlur();
89+ input.onBlur();
7690 }
7791 },
78− [addToChips, value]
92+ [addToChips, value, input]
7993 );
8094
8195 const hasError = hasErrors(error);
@@ -89,11 +103,16 @@ const EmailChipInput = ({ className, name, label }) => {
89103 )}
90104 <Flex.Item
91105 className={cn('flex b1 flex-wrap', {
92− 'b-green2': active && !hasError,
93− 'b-red3': active && hasError,
106+ 'b-green2': active && !disabled && !hasError,
107+ 'b-red3': active && !disabled && hasError,
94108 })}>
95109 {chips.map((chip, idx) => (
96− <Chip key={idx} value={chip} onDelete={handleDelete(idx)} />
110+ <Chip
111+ key={idx}
112+ value={chip}
113+ disabled={disabled}
114+ onDelete={handleDelete(idx)}
115+ />
97116 ))}
98117
99118 <input
@@ -104,7 +123,6 @@ const EmailChipInput = ({ className, name, label }) => {
104123 name={name}
105124 onChange={handleChange}
106125 onKeyDown={handleKeyDown}
107− onFocus={input.onFocus}
108126 onBlur={handleBlur}
109127 />
110128 </Flex.Item>
src/form/validators.js+3
@@ -15,6 +15,7 @@ import {
1515 validateNotNullAddress,
1616 validateHexPrefix,
1717 validateLength,
18+ validateUnique,
1819} from 'lib/validators';
1920import isPromise from 'lib/isPromise';
2021
@@ -105,6 +106,8 @@ export const buildNumberValidator = (min = 0) =>
105106export const buildEmailValidator = validate =>
106107 buildValidator([validateNotEmpty, validateEmail], validate);
107108
109+export const buildEmailArrayValidator = () =>
110+ buildValidator([validateUnique, buildArrayValidator(buildEmailValidator())]);
108111// the form validator is the composition of all of the field validators
109112// plus an additional form validator function
110113export const composeValidator = (
src/lib/validators.js+5
@@ -102,3 +102,8 @@ export const validateNotNullAddress = validateNotAny([
102102 ETH_ZERO_ADDR,
103103 ETH_ZERO_ADDR_SHORT,
104104]);
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() {
142142 const isFailed = status === STATUS.FAILURE;
143143 const isDone = status === STATUS.SUCCESS;
144144
145− const validateHasReceived = useCallback(
146− async email => {
147− const hasReceived = await getHasReceived(email);
148− if (hasReceived) {
149− return HAS_RECEIVED_TEXT;
150− }
151− },
152− [getHasReceived]
153− );
145+ const validateHasReceived = useCallback(async email => {
146+ // const hasReceived = await getHasReceived(email);
147+ // if (hasReceived) {
148+ // return HAS_RECEIVED_TEXT;
149+ // }
150+ }, []);
154151
155152 const validateForm = useCallback((values, errors) => {
156153 if (hasErrors(errors)) {
@@ -384,12 +381,13 @@ export default function InviteEmail() {
384381 }, [
385382 web3,
386383 wallet,
387− point,
388384 gasPrice,
389− invites,
385+ point,
390386 clearReceipts,
387+ invites,
391388 addReceipt,
392389 sendMail,
390+ message,
393391 ]);
394392
395393 const onSubmit = useCallback(
src/views/Point.js+113−3
@@ -1,7 +1,10 @@
1−import React, { useCallback } from 'react';
2−import { Just } from 'folktale/maybe';
1+import React, { useCallback, useState } from 'react';
2+import { Just, Nothing } from 'folktale/maybe';
33import { Grid } from 'indigo-react';
44import { azimuth } from 'azimuth-js';
5+import cn from 'classnames';
6+import { take } from 'lodash';
7+import ob from 'urbit-ob';
58
69import { usePointCursor } from 'store/pointCursor';
710import { useWallet } from 'store/wallet';
@@ -13,6 +16,8 @@ import { ForwardButton, BootUrbitOSButton } from 'components/Buttons';
1316import CopyButton from 'components/CopyButton';
1417import { matchBlinky } from 'components/Blinky';
1518import DownloadSigilButton from 'components/DownloadSigilButton';
19+import BarGraph from 'components/BarGraph';
20+import MaybeSigil from 'components/MaybeSigil';
1621
1722import * as need from 'lib/need';
1823import useInvites from 'lib/useInvites';
@@ -21,6 +26,46 @@ import useCurrentPermissions from 'lib/useCurrentPermissions';
2126import { useLocalRouter } from 'lib/LocalRouter';
2227import useKeyfileGenerator from 'lib/useKeyfileGenerator';
2328
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+
2469export default function Point() {
2570 const { pop, push, names } = useLocalRouter();
2671 const { pointCursor } = usePointCursor();
@@ -38,7 +83,22 @@ export default function Point() {
3883 } = useCurrentPermissions();
3984
4085 // fetch the invites for the current cursor
41− const { availableInvites } = useInvites(point);
86+ const invites = useInvites(point);
87+ const {
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
42102
43103 const goAdmin = useCallback(() => push(names.ADMIN), [push, names]);
44104
@@ -54,6 +114,10 @@ export default function Point() {
54114 push,
55115 ]);
56116
117+ const isPlanet = azimuth.getPointSize(point) === azimuth.PointSize.Planet;
118+
119+ const [showInviteForm, setShowInviteForm] = useState(false);
120+
57121 const inviteButton = (() => {
58122 switch (azimuth.getPointSize(point)) {
59123 case azimuth.PointSize.Planet:
@@ -103,6 +167,52 @@ export default function Point() {
103167 address={Just(address)}
104168 animationMode={'slide'}
105169 />
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>
106216 <Grid className="pt2">
107217 {inviteButton}
108218 <Grid.Item