import React, { useMemo, useEffect, useCallback, useState } from 'react'; import { Grid, Flex, Button } from 'indigo-react'; import * as ob from 'urbit-ob'; import { Just, Nothing } from 'folktale/maybe'; import cn from 'classnames'; import { delegatedSending } from 'azimuth-js'; import { usePointCursor } from 'store/pointCursor'; import { usePointCache } from 'store/pointCache'; import { useWallet } from 'store/wallet'; import View from 'components/View'; import Tabs from 'components/Tabs'; import BarGraph from 'components/BarGraph'; import MaybeSigil from 'components/MaybeSigil'; import { matchBlinky } from 'components/Blinky'; import Chip from 'components/Chip'; import Crumbs from 'components/Crumbs'; import { useLocalRouter } from 'lib/LocalRouter'; import useInvites from 'lib/useInvites'; import * as need from 'lib/need'; import * as wg from 'lib/walletgen'; import Inviter from 'views/Invite/Inviter'; import { ReactComponent as SearchIcon } from 'assets/search.svg'; import CopyButton from 'components/CopyButton'; import { useSyncForeignPoints } from 'lib/useSyncPoints'; import { useNetwork } from 'store/network'; import NavHeader from 'components/NavHeader'; function SearchInput({ className, value, onChange }) { return ( ); } function CohortMember({ point, pending = false, className, ...rest }) { const { sentInvites } = useInvites(point); const patp = useMemo(() => ob.patp(point), [point]); const colors = pending ? ['#ee892b', '#FFFFFF'] : ['#000000', '#FFFFFF']; const DetailText = useCallback( () => pending ? 'Pending' : <> {matchBlinky(sentInvites)} points invited , [pending, sentInvites] ); return ( {patp} ); } function CohortList({ acceptedPoints, pendingPoints, className, onlyPending, onSelectInvite, }) { const { syncInvites } = usePointCache(); const [query, setQuery] = useState(''); useEffect(() => { acceptedPoints.map(syncInvites); }, [acceptedPoints, syncInvites]); const handleChange = useCallback( e => { setQuery(e.target.value); e.preventDefault(); }, [setQuery] ); const filterPoints = useCallback( points => points.filter(p => { const patp = ob.patp(p).slice(1); return patp.startsWith(query); }), [query] ); const selectInvite = useCallback( point => () => { onSelectInvite(point); }, [onSelectInvite] ); const _pendingPoints = filterPoints(pendingPoints); const _acceptedPoints = filterPoints(acceptedPoints); const offset = onlyPending ? 0 : _acceptedPoints.length; return ( <> {!onlyPending && _acceptedPoints.map((p, idx) => ( ))} {_pendingPoints.map((p, idx) => ( ))} {_pendingPoints.length === 0 && (_acceptedPoints.length === 0 || onlyPending) && ( {' '} No invites accepted yet. )} ); } function hideIf(hider, hidden) { return hider ? '●●●●●●●' : hidden; } function CohortMemberExpanded({ point, className, ...rest }) { const { sentInvites, acceptedInvites } = useInvites(point); const { getDetails } = usePointCache(); const { authToken } = useWallet(); const { contracts } = useNetwork(); const details = getDetails(point); const { active } = details.getOrElse({}); const patp = useMemo(() => ob.patp(point), [point]); const colors = useMemo( () => (!active ? ['#ee892b', '#FFFFFF'] : ['#000000', '#FFFFFF']), [active] ); const [code, setCode] = useState(Nothing()); const [codeVisible, setCodeVisible] = useState(false); useEffect(() => { const fetchWallet = async () => { const _authToken = authToken.getOrElse(null); const _details = details.getOrElse(null); const _contracts = contracts.getOrElse(null); if ( !_authToken || !_details || _details.active || !_contracts || !codeVisible ) { setCode(Nothing()); return; } const { ticket, owner } = await wg.generateTemporaryDeterministicWallet( point, _authToken ); if (owner.keys.address !== _details.transferProxy) { setCode(Just(null)); } else { setCode(Just(ticket)); } }; fetchWallet(); }, [authToken, codeVisible, contracts, details, point]); useEffect(() => { setCodeVisible(false); }, [point]); return ( {patp} {!active && ( <> Invite Code {(code.getOrElse(true) === null && 'This invite code cannot be recovered') || hideIf(!codeVisible, matchBlinky(code))} {!codeVisible && ( setCodeVisible(true)}> Show )} {codeVisible && code.getOrElse('') !== null && ( )} )} {active && ( <> {matchBlinky(sentInvites)} invites sent {matchBlinky(acceptedInvites)} invites accepted )} ); } export default function InviteCohort() { const { pop } = useLocalRouter(); const { pointCursor } = usePointCursor(); const point = need.point(pointCursor); const { syncDetails } = usePointCache(); const { acceptedInvites, acceptedPoints, pendingPoints, availableInvites, sentInvites, } = useInvites(point); const [showInviteForm, setShowInviteForm] = useState(false); const [tab, setTab] = useState(NAMES.ALL); const _acceptedPoints = acceptedPoints.getOrElse([]); const _pendingPoints = pendingPoints.getOrElse([]); const _pendingInvites = _pendingPoints.length; const _acceptedInvites = _acceptedPoints.length; const _totalInvites = availableInvites .map(a => sentInvites.getOrElse(0) + a) .getOrElse(0); const [selectedInvite, _setSelectedInvite] = useState(); useSyncForeignPoints([..._acceptedPoints, ..._pendingPoints]); const setSelectedInvite = useCallback( p => _setSelectedInvite(old => (old === p ? null : p)), [] ); return ( pop() }, { text: 'Invite Group' }, ]} /> Invite Group {_acceptedInvites} / {_totalInvites} {_pendingInvites !== 0 && ( {_pendingInvites} pending )} {availableInvites.getOrElse(0) !== 0 && !showInviteForm && ( setShowInviteForm(true)}> Add Members )} {showInviteForm && } {true && ( )} {selectedInvite && ( setSelectedInvite(null)} /> )} ); } const NAMES = { ALL: 'ALL', PENDING: 'PENDING', }; const VIEWS = { [NAMES.PENDING]: CohortList, [NAMES.ALL]: CohortList, }; const OPTIONS = [ { text: 'All', value: NAMES.ALL }, { text: 'Pending', value: NAMES.PENDING }, ];