import React, { useCallback, useEffect, useMemo } from 'react'; import { Just, Nothing } from 'folktale/maybe'; import { Grid, H5, HelpText, LinkButton, Flex } from 'indigo-react'; import { get } from 'lodash'; import { useHistory } from 'store/history'; import { useWallet } from 'store/wallet'; import { usePointCache } from 'store/pointCache'; import { usePointCursor } from 'store/pointCursor'; import { useStarReleaseCache } from 'store/starRelease'; import * as need from 'lib/need'; import { isZeroAddress, abbreviateAddress } from 'lib/utils/address'; import useIsEclipticOwner from 'lib/useIsEclipticOwner'; import { useSyncDetails } from 'lib/useSyncPoints'; import useRejectedIncomingPointTransfers from 'lib/useRejectedIncomingPointTransfers'; import { pluralize } from 'lib/pluralize'; import newGithubIssueUrl from 'new-github-issue-url'; import View from 'components/View'; import Blinky from 'components/Blinky'; import Passport from 'components/Passport'; import Footer from 'components/Footer'; import { ForwardButton } from 'components/Buttons'; import CopiableAddress from 'components/copiable/CopiableAddress'; import NavHeader from 'components/NavHeader'; import L2PointHeader from 'components/L2/Headers/L2PointHeader'; import IncomingPoint from 'components/L2/Points/IncomingPoint'; import { useNetwork } from 'store/network'; export const maybeGetResult = (obj, key, defaultValue) => obj.matchWith({ Nothing: () => defaultValue, Just: p => p.value.matchWith({ Ok: r => get(r.value, key, defaultValue), Error: e => defaultValue, }), }); export const hasTransferProxy = details => !isZeroAddress(details.transferProxy); export const getOutgoingPoints = (controlledPoints, getDetails) => { return controlledPoints .chain(points => points.matchWith({ Error: () => Nothing(), Ok: c => { const points = c.value.ownedPoints.map(point => getDetails(point).chain(details => Just({ point: point, has: hasTransferProxy(details) }) ) ); if (points.every(p => Just.hasInstance(p))) { const outgoing = points .filter(p => p.value.has) .map(p => p.value.point); return Just(outgoing); } else { return Nothing(); } }, }) ) .getOrElse([]); }; export const isLocked = (details, contracts) => details.owner === contracts.linearSR || details.owner === contracts.conditionalSR; const PointList = function({ points, className, actions, locked = false, ...rest }) { const { setPointCursor } = usePointCursor(); const { push, names } = useHistory(); return ( {points.map((point, i) => ( { setPointCursor(Just(point)); push(names.POINT); } } {...rest} /> {actions && ( {actions(point, i)} )} ))} ); }; function ActionButtons({ actions = [] }) { return ( {actions.map(action => ( {action.text} ))} ); } export default function Points() { const { wallet } = useWallet(); const { pop, push, popAndPush, names } = useHistory(); const { setPointCursor } = usePointCursor(); const { controlledPoints, getDetails } = usePointCache(); const { contracts } = useNetwork(); const isEclipticOwner = useIsEclipticOwner(); const [ rejectedPoints, addRejectedPoint, ] = useRejectedIncomingPointTransfers(); const { syncStarReleaseDetails, starReleaseDetails } = useStarReleaseCache(); const outgoingPoints = useMemo( () => getOutgoingPoints(controlledPoints, getDetails), [getDetails, controlledPoints] ); const _contracts = need.contracts(contracts); const maybeLockedPoints = useMemo( () => controlledPoints.chain(points => points.matchWith({ Error: () => Nothing(), Ok: c => { const points = c.value.ownedPoints.map(point => getDetails(point).chain(details => Just({ point, has: isLocked(details, _contracts) }) ) ); // if we have details for every point, // return the array of pending transfers. if (points.every(p => Just.hasInstance(p))) { const locked = points .filter(p => p.value.has) .map(p => p.value.point); return Just(locked); } else { return Nothing(); } }, }) ), [getDetails, controlledPoints, _contracts] ); // if we can only interact with a single point, forget about the existence // of this page and jump to the point page. // if there are any pending transfers, incoming or outgoing, stay on this // page, because those can only be completed/cancelled here. useEffect(() => { if (outgoingPoints.length < 1 || Nothing.hasInstance(starReleaseDetails)) { return; } controlledPoints.matchWith({ Nothing: () => null, Just: r => { r.value.matchWith({ Error: () => null, Ok: c => { let all = [ ...c.value.ownedPoints, ...c.value.votingPoints, ...c.value.managingPoints, ...c.value.spawningPoints, ]; const incoming = c.value.incomingPoints.filter( p => !rejectedPoints.includes(p) ); if ( all.length === 1 && incoming.length === 0 && outgoingPoints.length === 0 && (starReleaseDetails.value === null || starReleaseDetails.value.total === 0) ) { setPointCursor(Just(all[0])); popAndPush(names.POINT); } }, }); }, }); }, [ controlledPoints, rejectedPoints, outgoingPoints, setPointCursor, popAndPush, names, starReleaseDetails, ]); const address = need.addressFromWallet(wallet); const loading = Nothing.hasInstance(controlledPoints); const ownedPoints = maybeGetResult(controlledPoints, 'ownedPoints', []); const incomingPoints = maybeGetResult( controlledPoints, 'incomingPoints', [] ).filter(point => !rejectedPoints.includes(point)); const managingPoints = maybeGetResult(controlledPoints, 'managingPoints', []); const votingPoints = maybeGetResult(controlledPoints, 'votingPoints', []); const spawningPoints = maybeGetResult(controlledPoints, 'spawningPoints', []); const lockedPoints = maybeLockedPoints.getOrElse([]); const allPoints = [ ...ownedPoints.filter(p => !outgoingPoints.includes(p)), ...managingPoints, ...votingPoints, ...spawningPoints, ].sort((a, b) => Number(a) - Number(b)); const displayEmptyState = !loading && incomingPoints.length === 0 && allPoints.length === 0; const starReleasing = starReleaseDetails .map(s => (s ? s.total > 0 : false)) .getOrElse(false); useEffect(() => { syncStarReleaseDetails(); }, [syncStarReleaseDetails]); // sync display details for known points useSyncDetails(ownedPoints); const goCreateGalaxy = useCallback(() => push(names.CREATE_GALAXY), [ names.CREATE_GALAXY, push, ]); const goStarRelease = useCallback(() => push(names.STAR_RELEASE), [ names.STAR_RELEASE, push, ]); // if we got an error result, we should display it, instead of showing // a potentially inaccurately empty point list. // if ( Just.hasInstance(controlledPoints) && controlledPoints.value.matchWith({ Error: e => true, Ok: v => false, }) ) { const url = newGithubIssueUrl({ user: 'urbit', repo: 'bridge', title: controlledPoints.value.value, body: ``, }); return ( {controlledPoints.value.value}
Try reloading the page. If this problem persists, please{' '} file an issue on GitHub.
); } if ( loading || (allPoints.length === 1 && incomingPoints.length === 0 && outgoingPoints.length === 0 && !starReleasing) ) { return ( Loading... ); } return ( }> {abbreviateAddress(address)} {incomingPoints.map(point => ( { setPointCursor(Just(point)); push(names.ACCEPT_TRANSFER); }} reject={() => addRejectedPoint(point)} /> ))} {displayEmptyState && ( No points to display. This wallet is not the owner or proxy for any points. )} {outgoingPoints.length > 0 && ( {pluralize( outgoingPoints.length, 'Outgoing Transfer', 'Outgoing Transfers' )} ( { setPointCursor(Just(point)); // TODO: deep linking to fix this duplicate route push(names.CANCEL_TRANSFER); }, }, ]} /> )} /> )} {allPoints.length > 0 && ( )} {lockedPoints.length > 0 && ( )} ); }