diff --git a/pkg/interface/src/logic/lib/useRunIO.ts b/pkg/interface/src/logic/lib/useRunIO.ts new file mode 100644 index 0000000000..12d8628cd6 --- /dev/null +++ b/pkg/interface/src/logic/lib/useRunIO.ts @@ -0,0 +1,52 @@ +import { useState, useEffect } from "react"; +import { useWaitForProps } from "./useWaitForProps"; +import {unstable_batchedUpdates} from "react-dom"; + +export type IOInstance = ( + input: I +) => (props: P) => Promise<[(p: P) => boolean, O]>; + +export function useRunIO( + io: (i: I) => Promise, + after: (o: O) => void, + key: string +) { + const [resolve, setResolve] = useState<() => void>(() => () => {}); + const [reject, setReject] = useState<(e: any) => void>(() => () => {}); + const [output, setOutput] = useState(null); + const [done, setDone] = useState(false); + const run = (i: I) => + new Promise((res, rej) => { + setResolve(() => res); + setReject(() => rej); + io(i) + .then((o) => { + unstable_batchedUpdates(() => { + setOutput(o); + setDone(true); + }); + }) + .catch(rej); + }); + + useEffect(() => { + reject(new Error("useRunIO: key changed")); + setDone(false); + setOutput(null); + }, [key]); + + useEffect(() => { + if (!done) { + return; + } + try { + after(output!); + resolve(); + } catch (e) { + reject(e); + } + }, [done]); + + return run; +} + diff --git a/pkg/interface/src/views/components/Invite/index.tsx b/pkg/interface/src/views/components/Invite/index.tsx index a8d90f7813..144daf7141 100644 --- a/pkg/interface/src/views/components/Invite/index.tsx +++ b/pkg/interface/src/views/components/Invite/index.tsx @@ -1,27 +1,28 @@ -import React, { useState, useEffect, useCallback } from 'react'; -import { useHistory } from 'react-router-dom'; +import React, { useState, useEffect, useCallback } from "react"; +import { useHistory } from "react-router-dom"; import { MetadataUpdatePreview, Contacts, JoinRequests, Groups, - Associations -} from '@urbit/api'; -import { Invite } from '@urbit/api/invite'; -import { Text, Icon, Row } from '@tlon/indigo-react'; + Associations, +} from "@urbit/api"; +import { Invite } from "@urbit/api/invite"; +import { Text, Icon, Row } from "@tlon/indigo-react"; -import { cite, useShowNickname } from '~/logic/lib/util'; -import GlobalApi from '~/logic/api/global'; -import { resourceFromPath } from '~/logic/lib/group'; -import { GroupInvite } from './Group'; -import { InviteSkeleton } from './InviteSkeleton'; -import { JoinSkeleton } from './JoinSkeleton'; -import { useWaitForProps } from '~/logic/lib/useWaitForProps'; -import useGroupState from '~/logic/state/group'; -import useContactState from '~/logic/state/contact'; -import useMetadataState from '~/logic/state/metadata'; -import useGraphState from '~/logic/state/graph'; +import { cite, useShowNickname } from "~/logic/lib/util"; +import GlobalApi from "~/logic/api/global"; +import { resourceFromPath } from "~/logic/lib/group"; +import { GroupInvite } from "./Group"; +import { InviteSkeleton } from "./InviteSkeleton"; +import { JoinSkeleton } from "./JoinSkeleton"; +import { useWaitForProps } from "~/logic/lib/useWaitForProps"; +import useGroupState from "~/logic/state/group"; +import useContactState from "~/logic/state/contact"; +import useMetadataState from "~/logic/state/metadata"; +import useGraphState from "~/logic/state/graph"; +import { useRunIO } from "~/logic/lib/useRunIO"; interface InviteItemProps { invite?: Invite; @@ -32,62 +33,81 @@ interface InviteItemProps { api: GlobalApi; } +export function useInviteAccept( + resource: string, + api: GlobalApi, + app?: string, + uid?: string, + invite?: Invite, +) { + const { ship, name } = resourceFromPath(resource); + const history = useHistory(); + const associations = useMetadataState((s) => s.associations); + const groups = useGroupState((s) => s.groups); + const graphKeys = useGraphState((s) => s.graphKeys); + + const waiter = useWaitForProps({ associations, graphKeys, groups }); + return useRunIO( + async () => { + if (!(app && invite && uid)) { + return false; + } + if (resource in groups) { + await api.invite.decline(app, uid); + return false; + } + + await api.groups.join(ship, name); + await api.invite.accept(app, uid); + await waiter((p) => { + return ( + (resource in p.groups && + resource in (p.associations?.graph ?? {}) && + p.graphKeys.has(resource.slice(7))) || + resource in (p.associations?.groups ?? {}) + ); + }); + return true; + }, + (success: boolean) => { + if (!success) { + return; + } + if (groups?.[resource]?.hidden) { + const { metadata } = associations.graph[resource]; + if (metadata && "graph" in metadata.config) { + if (metadata.config.graph === "chat") { + history.push( + `/~landscape/messages/resource/${metadata.config.graph}${resource}` + ); + } else { + history.push( + `/~landscape/home/resource/${metadata.config.graph}${resource}` + ); + } + } else { + console.error("unknown metadata: ", metadata); + } + } else { + history.push(`/~landscape${resource}`); + } + }, + resource + ); +} + export function InviteItem(props: InviteItemProps) { const [preview, setPreview] = useState(null); const { pendingJoin, invite, resource, uid, app, api } = props; - const { ship, name } = resourceFromPath(resource); - const groups = useGroupState(state => state.groups); - const graphKeys = useGraphState(s => s.graphKeys); - const associations = useMetadataState(state => state.associations); - const contacts = useContactState(state => state.contacts); + const { name } = resourceFromPath(resource); + const contacts = useContactState((state) => state.contacts); const contact = contacts?.[`~${invite?.ship}`] ?? {}; const showNickname = useShowNickname(contact); - const waiter = useWaitForProps( - { associations, groups, pendingJoin, graphKeys: Array.from(graphKeys) }, - 50000 - ); - const history = useHistory(); - const inviteAccept = useCallback(async () => { - if (!(app && invite && uid)) { - return; - } - if(resource in groups) { - await api.invite.decline(app, uid); - return; - } - - api.groups.join(ship, name); - await waiter(p => !!p.pendingJoin); - - api.invite.accept(app, uid); - await waiter((p) => { - return ( - resource in p.groups && - (resource in (p.associations?.graph ?? {}) || - resource in (p.associations?.groups ?? {})) - ); - }); - - if (groups?.[resource]?.hidden) { - await waiter(p => p.graphKeys.includes(resource.slice(7))); - const { metadata } = associations.graph[resource]; - if (metadata && 'graph' in metadata.config) { - if (metadata.config.graph === 'chat') { - history.push(`/~landscape/messages/resource/${metadata.config.graph}${resource}`); - } else { - history.push(`/~landscape/home/resource/${metadata.config.graph}${resource}`); - } - } else { - console.error('unknown metadata: ', metadata); - } - } else { - history.push(`/~landscape${resource}`); - } - }, [app, history, waiter, invite, uid, resource, groups, associations]); + const inviteAccept = useInviteAccept(resource, api, app, uid, invite); const inviteDecline = useCallback(async () => { - if(!(app && uid)) { + if (!(app && uid)) { return; } await api.invite.decline(app, uid); @@ -96,7 +116,7 @@ export function InviteItem(props: InviteItemProps) { const handlers = { onAccept: inviteAccept, onDecline: inviteDecline }; useEffect(() => { - if (!app || app === 'groups') { + if (!app || app === "groups") { (async () => { setPreview(await api.metadata.preview(resource)); })(); @@ -108,7 +128,7 @@ export function InviteItem(props: InviteItemProps) { } }, [invite]); - if(pendingJoin?.hidden) { + if (pendingJoin?.hidden) { return null; } @@ -123,7 +143,7 @@ export function InviteItem(props: InviteItemProps) { {...handlers} /> ); - } else if (invite && name.startsWith('dm--')) { + } else if (invite && name.startsWith("dm--")) { return ( - + fontWeight={showNickname ? "500" : "400"} + > {showNickname ? contact?.nickname : cite(`~${invite!.ship}`)} invited you to a DM ); - } else if (status && name.startsWith('dm--')) { + } else if (status && name.startsWith("dm--")) { return ( @@ -162,9 +184,11 @@ export function InviteItem(props: InviteItemProps) { > - + fontWeight={showNickname ? "500" : "400"} + > {showNickname ? contact?.nickname : cite(`~${invite!.ship}`)} @@ -174,14 +198,12 @@ export function InviteItem(props: InviteItemProps) { ); } else if (pendingJoin) { - const [, , ship, name] = resource.split('/'); + const [, , ship, name] = resource.split("/"); return ( - - You are joining - + You are joining {cite(ship)}/{name}