@@ -1,5 +1,5 @@
11import React, { useMemo, useEffect, useCallback, useState } from 'react';
2−import { Grid, Flex } from 'indigo-react';
2+import { Grid, Flex, Button } from 'indigo-react';
33import * as ob from 'urbit-ob';
44import { Just } from 'folktale/maybe';
55import cn from 'classnames';
@@ -8,10 +8,11 @@ import { usePointCursor } from 'store/pointCursor';
88import { usePointCache } from 'store/pointCache';
99
1010import View from 'components/View';
11+import Tabs from 'components/Tabs';
1112import BarGraph from 'components/BarGraph';
12−import InviteSigilList from 'components/InviteSigilList';
1313import MaybeSigil from 'components/MaybeSigil';
1414import { matchBlinky } from 'components/Blinky';
15+import { ForwardButton } from 'components/Buttons';
1516import Chip from 'components/Chip';
1617import Crumbs from 'components/Crumbs';
1718
@@ -19,12 +20,38 @@ import { useLocalRouter } from 'lib/LocalRouter';
1920import useInvites from 'lib/useInvites';
2021import * as need from 'lib/need';
2122
23+import Inviter from 'views/Invite/Inviter';
24+
2225import { ReactComponent as SearchIcon } from 'assets/search.svg';
2326
24−function CohortMember({ point, className }) {
27+function SearchInput({ className, value, onChange }) {
28+ return (
29+ <Flex align="center" full className={cn('b b-gray2 b1', className)}>
30+ <Flex.Item className="p2">
31+ <SearchIcon />
32+ </Flex.Item>
33+
34+ <Flex.Item
35+ flex={1}
36+ as={'input'}
37+ value={value}
38+ onChange={onChange}
39+ className="b-none h7"
40+ />
41+ </Flex>
42+ );
43+}
44+
45+function CohortMember({ point, pending = false, className }) {
2546 const { sentInvites } = useInvites(point);
2647 const patp = useMemo(() => ob.patp(point), [point]);
27− const colors = ['#000000', '#FFFFFF'];
48+ const colors = pending ? ['#ee892b', '#FFFFFF'] : ['#000000', '#FFFFFF'];
49+
50+ const DetailText = useCallback(
51+ () =>
52+ pending ? 'Pending' : <> {matchBlinky(sentInvites)} points invited </>,
53+ [sentInvites]
54+ );
2855
2956 return (
3057 <Flex justify="between" className={cn('b1 b-gray2', className)}>
@@ -39,19 +66,21 @@ function CohortMember({ point, className }) {
3966 col
4067 className="mono f6 ph4">
4168 <Flex.Item>{patp}</Flex.Item>
42− <Flex.Item>{matchBlinky(sentInvites)} points invited</Flex.Item>
69+ <Flex.Item className="gray4">
70+ <DetailText />
71+ </Flex.Item>
4372 </Flex.Item>
4473 </Flex>
4574 );
4675}
47−function CohortList({ points, className }) {
76+function CohortList({ acceptedPoints, pendingPoints, className, onlyPending }) {
4877 const { syncInvites } = usePointCache();
4978
5079 const [query, setQuery] = useState('');
5180
5281 useEffect(() => {
53− points.map(syncInvites);
54− }, [points, syncInvites]);
82+ acceptedPoints.map(syncInvites);
83+ }, [acceptedPoints, syncInvites]);
5584
5685 const handleChange = useCallback(
5786 e => {
@@ -61,40 +90,51 @@ function CohortList({ points, className }) {
6190 [setQuery]
6291 );
6392
64− const visiblePoints = useMemo(
65− () =>
93+ const filterPoints = useCallback(
94+ points =>
6695 points.filter(p => {
6796 const patp = ob.patp(p).slice(1);
6897 return patp.startsWith(query);
6998 }),
70− [query, points]
99+ [query]
71100 );
72101
73− return (
74− <Grid gap={3} className={cn('mt1', className)}>
75− <Grid.Item align="center" full as={Flex} className="b b-gray2 b1">
76− <Flex.Item>
77− <SearchIcon className="p2" />
78− </Flex.Item>
102+ const _pendingPoints = filterPoints(pendingPoints);
103+ const _acceptedPoints = filterPoints(acceptedPoints);
79104
80− <Flex.Item
81− flex={1}
82− as={'input'}
83− value={query}
84− onChange={handleChange}
85− className="b-none"
86− />
87− </Grid.Item>
105+ const offset = onlyPending ? 0 : _acceptedPoints.length;
106+
107+ return (
108+ <Grid gap={3} className={cn('mt4', className)}>
109+ <Grid.Item full as={SearchInput} value={query} onChange={handleChange} />
88110 <>
89− {visiblePoints.map((p, idx) => (
90− <Grid.Item key={p} half={(idx % 2) + 1} as={CohortMember} point={p} />
111+ {!onlyPending &&
112+ _acceptedPoints.map((p, idx) => (
113+ <Grid.Item
114+ key={p}
115+ half={(idx % 2) + 1}
116+ as={CohortMember}
117+ point={p}
118+ />
119+ ))}
120+ {_pendingPoints.map((p, idx) => (
121+ <Grid.Item
122+ key={p}
123+ half={((offset + idx) % 2) + 1}
124+ as={CohortMember}
125+ point={p}
126+ pending
127+ />
91128 ))}
92129 </>
93− {visiblePoints.length === 0 && (
94− <Grid.Item full className="p4 t-center">
95− No invites accepted yet.
96− </Grid.Item>
97− )}
130+
131+ {_pendingPoints.length === 0 &&
132+ (_acceptedPoints.length === 0 || onlyPending) && (
133+ <Grid.Item full className="p4 t-center">
134+ {' '}
135+ No invites accepted yet.
136+ </Grid.Item>
137+ )}
98138 </Grid>
99139 );
100140}
@@ -113,6 +153,9 @@ export default function InviteCohort() {
113153 sentInvites,
114154 } = useInvites(point);
115155
156+ const [showInviteForm, setShowInviteForm] = useState(false);
157+ const [tab, setTab] = useState(NAMES.ALL);
158+
116159 const _acceptedPoints = acceptedPoints.getOrElse([]);
117160 const _pendingPoints = pendingPoints.getOrElse([]);
118161 const _pendingInvites = _pendingPoints.length;
@@ -138,10 +181,10 @@ export default function InviteCohort() {
138181 <Grid.Item full>
139182 <Flex align="center">
140183 <Flex.Item>
141− {_acceptedInvites + _pendingInvites} / {_totalInvites}
184+ {_acceptedInvites} / {_totalInvites}
142185 </Flex.Item>
143− {_pendingInvites && (
144− <Flex.Item as={Chip} color="yellow">
186+ {_pendingInvites !== 0 && (
187+ <Flex.Item as={Chip} className="bg-yellow4 white">
145188 {_pendingInvites} pending
146189 </Flex.Item>
147190 )}
@@ -154,14 +197,45 @@ export default function InviteCohort() {
154197 sent={sentInvites}
155198 accepted={acceptedInvites}
156199 />
200+ {availableInvites.getOrElse(0) !== 0 && !showInviteForm && (
201+ <Grid.Item
202+ full
203+ solid
204+ center
205+ as={Button}
206+ onClick={() => setShowInviteForm(true)}>
207+ Add Members
208+ </Grid.Item>
209+ )}
210+ {showInviteForm && <Inviter />}
211+
157212 <Grid.Item
158213 full
159− as={InviteSigilList}
160− pendingPoints={pendingPoints}
161− acceptedPoints={acceptedPoints}
214+ center
215+ as={Tabs}
216+ views={VIEWS}
217+ options={OPTIONS}
218+ currentTab={tab}
219+ onTabChange={setTab}
220+ acceptedPoints={_acceptedPoints}
221+ pendingPoints={_pendingPoints}
222+ onlyPending={tab === NAMES.PENDING}
162223 />
163− <Grid.Item full as={CohortList} points={_acceptedPoints} />
164224 </Grid>
165225 </View>
166226 );
167227}
228+const NAMES = {
229+ ALL: 'ALL',
230+ PENDING: 'PENDING',
231+};
232+
233+const VIEWS = {
234+ [NAMES.PENDING]: CohortList,
235+ [NAMES.ALL]: CohortList,
236+};
237+
238+const OPTIONS = [
239+ { text: 'All', value: NAMES.ALL },
240+ { text: 'Pending', value: NAMES.PENDING },
241+];