import React, { useCallback, useEffect, useMemo } from 'react'; import { Just, Nothing } from 'folktale/maybe'; import { Grid, H5, HelpText, LinkButton, Flex } from 'indigo-react'; 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 { useNetwork } from 'store/network'; import { useRollerStore } from 'store/rollerStore'; import * as need from 'lib/need'; import { 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 Point from 'lib/types/Point'; import newGithubIssueUrl from 'new-github-issue-url'; import View from 'components/View'; 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 LoadingOverlay from 'components/L2/LoadingOverlay'; import PointList from 'components/L2/PointList'; import { Box, Icon, Text } from '@tlon/indigo-react'; import './Points.scss'; import { StarReleaseButton } from './Points/StarReleaseButton'; import { maybeGetResult } from 'lib/maybeGetResult'; import { isGalaxy, isStar } from 'lib/utils/point'; import { L2VulnAlert } from 'components/alerts/L2VulnAlert'; export const isLocked = (details: any, contracts: any) => details.owner === contracts.linearSR || details.owner === contracts.conditionalSR; interface ActionButtonProps { actions?: { text?: string; onClick?: () => void; }[]; } function ActionButtons({ actions = [] }: ActionButtonProps) { return ( {actions.map(action => ( {action.text} ))} ); } export default function Points() { const { wallet }: any = useWallet(); const { pop, push, names }: any = useHistory(); const { setPointCursor }: any = usePointCursor(); const { controlledPoints, getDetails }: any = usePointCache(); const { contracts }: any = useNetwork(); const isEclipticOwner = useIsEclipticOwner(); const [ rejectedPoints, addRejectedPoint, ] = useRejectedIncomingPointTransfers(); const { pointList } = useRollerStore(); const { syncStarReleaseDetails, starReleaseDetails, }: any = useStarReleaseCache(); const outgoingPoints = useMemo( () => pointList.filter(({ isOutgoing }) => isOutgoing), [pointList] ); const incomingPoints = useMemo( () => pointList.filter( ({ isTransferProxy, value }) => isTransferProxy && !rejectedPoints.includes(value) ), [pointList, rejectedPoints] ); const _contracts = need.contracts(contracts); const maybeLockedPoints = useMemo( () => controlledPoints.chain((points: any) => points.matchWith({ Error: () => Nothing(), Ok: (c: any) => { const points = c.value.ownedPoints.map((point: number) => getDetails(point).chain((details: any) => Just({ point, has: isLocked(details, _contracts) }) ) ); // if we have details for every point, // return the array of pending transfers. if (points.every((p: any) => Just.hasInstance(p))) { const locked = points .filter((p: any) => p.value.has) .map((p: any) => p.value.point); return Just(locked); } else { return Nothing(); } }, }) ), [getDetails, controlledPoints, _contracts] ); const allPoints = useMemo( () => pointList.filter( ({ isTransferProxy, isTransferProxySet, shouldDisplay, value }) => shouldDisplay && !(isTransferProxy || isTransferProxySet) && !rejectedPoints.includes(value) ), [pointList, rejectedPoints] ); // if we can only interact with a single point, 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 (Nothing.hasInstance(starReleaseDetails)) { return; } if ( allPoints.length === 1 && incomingPoints.length === 0 && outgoingPoints.length === 0 && (starReleaseDetails.value === null || starReleaseDetails.value.total === 0) ) { setPointCursor(Just(allPoints[0].value)); push(names.POINT); } }, [ allPoints, rejectedPoints, outgoingPoints, incomingPoints, setPointCursor, push, names, starReleaseDetails, ]); const address = need.addressFromWallet(wallet); const loading = Nothing.hasInstance(controlledPoints); const ownedPoints = maybeGetResult(controlledPoints, 'ownedPoints', []); const lockedPoints = maybeLockedPoints .getOrElse([]) .map((value: number) => ({ value })); const displayEmptyState = !loading && incomingPoints.length === 0 && allPoints.length === 0; // If the user has any stars or galaxies, give a polite heads up that // it can take some time for the roller be aware of L1 TXs const showMigrateNotice = !displayEmptyState && allPoints.some(p => isStar(p.value) || isGalaxy(p.value)); const starReleasing = starReleaseDetails .map((s: any) => (s ? s.total > 0 : false)) .getOrElse(false); useEffect(() => { syncStarReleaseDetails(); }, [syncStarReleaseDetails]); useEffect(() => { if ( 'Notification' in window && Notification.permission !== 'denied' && Notification.permission !== 'granted' ) { Notification.requestPermission(); } }, []); // eslint-disable-line react-hooks/exhaustive-deps // 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: any) => true, Ok: (v: any) => 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 ; } return ( }> {abbreviateAddress(address)} {incomingPoints.map(({ value }) => ( { setPointCursor(Just(value)); push(names.ACCEPT_TRANSFER); }} reject={() => addRejectedPoint(value)} /> ))} {displayEmptyState && ( No Urbit IDs here yet )} {outgoingPoints.length > 0 && ( {pluralize( outgoingPoints.length, 'Outgoing Transfer', 'Outgoing Transfers' )} ( { setPointCursor(Just(point.value)); // TODO: deep linking to fix this duplicate route push(names.CANCEL_TRANSFER); }, }, ]} /> )} /> )} {allPoints.length > 0 && ( )} {lockedPoints.length > 0 && ( )} {showMigrateNotice && ( Not seeing a recently-migrated Layer 2 point? It may take a few minutes for the public roller to be aware of the transaction. )} ); }