diff --git a/src/screens/Signup/index.tsx b/src/screens/Signup/index.tsx index fd27658ada..5185adcc85 100644 --- a/src/screens/Signup/index.tsx +++ b/src/screens/Signup/index.tsx @@ -16,7 +16,7 @@ import {createFullHandle} from '#/lib/strings/handles' import {logger} from '#/logger' import {useServiceQuery} from '#/state/queries/service' import {useAgent} from '#/state/session' -import {useStarterPackQuery} from 'state/queries/useStarterPackQuery' +import {useStarterPackQuery} from 'state/queries/starter-pack' import {useActiveStarterPack} from 'state/shell/starter-pack' import {LoggedOutLayout} from '#/view/com/util/layouts/LoggedOutLayout' import { diff --git a/src/screens/StarterPack/StarterPackLandingScreen.tsx b/src/screens/StarterPack/StarterPackLandingScreen.tsx index 01fe29bbc3..16b4461077 100644 --- a/src/screens/StarterPack/StarterPackLandingScreen.tsx +++ b/src/screens/StarterPack/StarterPackLandingScreen.tsx @@ -16,7 +16,7 @@ import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {createStarterPackGooglePlayUri} from 'lib/strings/starter-pack' import {isWeb} from 'platform/detection' import {useModerationOpts} from 'state/preferences/moderation-opts' -import {useStarterPackQuery} from 'state/queries/useStarterPackQuery' +import {useStarterPackQuery} from 'state/queries/starter-pack' import { useActiveStarterPack, useSetActiveStarterPack, diff --git a/src/screens/StarterPack/StarterPackScreen.tsx b/src/screens/StarterPack/StarterPackScreen.tsx index 5158996750..58cbaf3226 100644 --- a/src/screens/StarterPack/StarterPackScreen.tsx +++ b/src/screens/StarterPack/StarterPackScreen.tsx @@ -17,7 +17,7 @@ import {logEvent} from 'lib/statsig/statsig' import {isWeb} from 'platform/detection' import {RQKEY} from 'state/queries/list-members' import {useResolveDidQuery} from 'state/queries/resolve-uri' -import {useStarterPackQuery} from 'state/queries/useStarterPackQuery' +import {useStarterPackQuery} from 'state/queries/starter-pack' import {useAgent, useSession} from 'state/session' import * as Toast from '#/view/com/util/Toast' import {PagerWithHeader} from 'view/com/pager/PagerWithHeader' diff --git a/src/screens/StarterPack/Wizard/index.tsx b/src/screens/StarterPack/Wizard/index.tsx index e8c01fd5c9..ca82fdfc7a 100644 --- a/src/screens/StarterPack/Wizard/index.tsx +++ b/src/screens/StarterPack/Wizard/index.tsx @@ -8,7 +8,6 @@ import {useSafeAreaInsets} from 'react-native-safe-area-context' import { AppBskyActorDefs, AppBskyGraphDefs, - AppBskyGraphStarterpack, AtUri, ModerationOpts, } from '@atproto/api' @@ -18,28 +17,24 @@ import {msg, Plural, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useFocusEffect, useNavigation} from '@react-navigation/native' import {NativeStackScreenProps} from '@react-navigation/native-stack' -import {useQueryClient} from '@tanstack/react-query' import {logger} from '#/logger' import {HITSLOP_10} from 'lib/constants' -import {createStarterPackList} from 'lib/generate-starterpack' import {CommonNavigatorParams, NavigationProp} from 'lib/routes/types' import {logEvent} from 'lib/statsig/statsig' import {sanitizeDisplayName} from 'lib/strings/display-names' import {sanitizeHandle} from 'lib/strings/handles' import {enforceLen} from 'lib/strings/helpers' +import {parseStarterPackUri} from 'lib/strings/starter-pack' import {isAndroid, isNative, isWeb} from 'platform/detection' import {useModerationOpts} from 'state/preferences/moderation-opts' -import {invalidateActorStarterPacksQuery} from 'state/queries/actor-starter-packs' -import { - invalidateListMembersQuery, - useListMembersQuery, -} from 'state/queries/list-members' +import {useListMembersQuery} from 'state/queries/list-members' import {useProfileQuery} from 'state/queries/profile' import { - invalidateStarterPack, + useCreateStarterPackMutation, + useEditStarterPackMutation, useStarterPackQuery, -} from 'state/queries/useStarterPackQuery' +} from 'state/queries/starter-pack' import {useAgent, useSession} from 'state/session' import {useSetMinimalShellMode} from 'state/shell' import * as Toast from '#/view/com/util/Toast' @@ -120,14 +115,8 @@ export function Wizard({ return ( @@ -136,17 +125,13 @@ export function Wizard({ } function WizardInner({ - rkey, - createdAt: initialCreatedAt, - listUri: initialListUri, - listItems: initialListItems, + currentStarterPack, + currentListItems, profile, moderationOpts, }: { - rkey?: string - createdAt?: string - listUri?: string - listItems?: AppBskyGraphDefs.ListItemView[] + currentStarterPack?: AppBskyGraphDefs.StarterPackView + currentListItems?: AppBskyGraphDefs.ListItemView[] profile: AppBskyActorDefs.ProfileViewBasic moderationOpts: ModerationOpts }) { @@ -155,7 +140,6 @@ function WizardInner({ const t = useTheme() const setMinimalShellMode = useSetMinimalShellMode() const {setEnabled} = useKeyboardController() - const queryClient = useQueryClient() const [state, dispatch] = useWizardState() const agent = useAgent() const {currentAccount} = useSession() @@ -163,6 +147,7 @@ function WizardInner({ did: currentAccount?.did, staleTime: 0, }) + const parsed = parseStarterPackUri(currentStarterPack?.uri) React.useEffect(() => { navigation.setOptions({ @@ -218,178 +203,88 @@ function WizardInner({ } const currUiStrings = wizardUiStrings[state.currentStep] - const invalidateQueries = async () => { - if (!rkey) return - - if (initialListUri) { - await invalidateListMembersQuery({queryClient, uri: initialListUri}) - } - await invalidateActorStarterPacksQuery({ - queryClient, - did: currentAccount!.did, + const onSuccessCreate = (data: {uri: string; cid: string}) => { + const rkey = new AtUri(data.uri).rkey + logEvent('starterPack:create', { + setName: state.name != null, + setDescription: state.description != null, + profilesCount: state.profiles.length, + feedsCount: state.feeds.length, }) - await invalidateStarterPack({ - queryClient, - did: currentAccount!.did, + dispatch({type: 'SetProcessing', processing: false}) + navigation.replace('StarterPack', { + name: currentAccount!.handle, rkey, }) } + const onSuccessEdit = () => { + if (navigation.canGoBack()) { + navigation.goBack() + } else { + navigation.replace('StarterPack', { + name: currentAccount!.handle, + rkey: parsed!.rkey, + }) + } + } + + const onError = (e: Error) => { + logger.error('Failed to create starter pack', {safeMessage: e}) + dispatch({type: 'SetProcessing', processing: false}) + Toast.show(_(msg`Failed to create starter pack`)) + } + + const {mutate: createStarterPack} = useCreateStarterPackMutation({ + onSuccess: onSuccessCreate, + onError, + }) + const {mutate: editStarterPack} = useEditStarterPackMutation({ + onSuccess: onSuccessEdit, + onError, + }) + const submit = async () => { dispatch({type: 'SetProcessing', processing: true}) - - try { - if (rkey) { - // Editing an existing starter pack - let list: {uri: string; cid: string} | undefined = initialListUri - ? {uri: initialListUri, cid: ''} - : undefined - if (initialListUri) { - const removedItems = initialListItems?.filter( - i => !state.profiles.find(p => p.did === i.subject.did), - ) - if (removedItems && removedItems.length > 0) { - await agent.com.atproto.repo.applyWrites({ - repo: currentAccount!.did, - writes: removedItems.map(i => ({ - $type: 'com.atproto.repo.applyWrites#delete', - collection: 'app.bsky.graph.listitem', - rkey: new AtUri(i.uri).rkey, - })), - }) - } - - const addedProfiles = state.profiles.filter( - p => !initialListItems?.find(i => i.subject.did === p.did), - ) - - if (addedProfiles.length > 0) { - await agent.com.atproto.repo.applyWrites({ - repo: currentAccount!.did, - writes: addedProfiles.map(p => ({ - $type: 'com.atproto.repo.applyWrites#create', - collection: 'app.bsky.graph.listitem', - value: { - $type: 'app.bsky.graph.listitem', - subject: p.did, - list: list?.uri, - createdAt: new Date().toISOString(), - }, - })), - }) - } - } else { - list = await createStarterPackList({ - name: state.name ?? getDefaultName(), - description: state.description, - descriptionFacets: [], - profiles: state.profiles, - agent, - }) - } - - await agent.com.atproto.repo.putRecord({ - repo: currentAccount!.did, - collection: 'app.bsky.graph.starterpack', - rkey, - record: { - name: state.name ?? getDefaultName(), - description: state.description, - descriptionFacets: [], - list: list?.uri, - feeds: state.feeds.map(f => ({ - uri: f.uri, - })), - createdAt: initialCreatedAt, - updatedAt: new Date().toISOString(), - }, - validate: false, // TODO remove! - }) - - setTimeout(async () => { - await invalidateQueries() - if (navigation.canGoBack()) { - navigation.goBack() - } else { - navigation.replace('StarterPack', { - name: currentAccount!.handle, - rkey, - }) - } - dispatch({type: 'SetProcessing', processing: false}) - }, 2000) - } else { - // Creating a new starter pack - const list = await createStarterPackList({ - name: state.name ?? getDefaultName(), - description: state.description, - descriptionFacets: [], - profiles: state.profiles, - agent, - }) - const res = await agent.app.bsky.graph.starterpack.create( - { - repo: currentAccount!.did, - validate: false, - }, - { - name: state.name ?? getDefaultName(), - description: state.description, - descriptionFacets: [], - list: list.uri, - feeds: state.feeds.map(f => ({ - uri: f.uri, - })), - createdAt: new Date().toISOString(), - }, - ) - - logEvent('starterPack:create', { - setName: state.name != null, - setDescription: state.description != null, - profilesCount: state.profiles.length, - feedsCount: state.feeds.length, - }) - - const newRkey = new AtUri(res.uri).rkey - - setTimeout(async () => { - await invalidateQueries() - navigation.replace('StarterPack', { - name: currentAccount!.handle, - rkey: newRkey, - }) - dispatch({type: 'SetProcessing', processing: false}) - }, 2000) - } - } catch (e: unknown) { - logger.error('Failed to create starter pack', {safeMessage: e}) - Toast.show(_(msg`Failed to create starter pack`)) - dispatch({type: 'SetProcessing', processing: false}) - return + if (currentStarterPack && currentListItems) { + editStarterPack({ + name: state.name ?? getDefaultName(), + description: state.description, + descriptionFacets: [], + profiles: state.profiles, + feeds: state.feeds, + currentStarterPack: currentStarterPack, + currentListItems: currentListItems, + }) + } else { + createStarterPack({ + name: state.name ?? getDefaultName(), + description: state.description, + descriptionFacets: [], + profiles: state.profiles, + feeds: state.feeds, + }) } } const deleteStarterPack = async () => { - if (!rkey || !initialListUri) return + if (!currentStarterPack || !currentListItems) return dispatch({type: 'SetProcessing', processing: true}) try { await agent.app.bsky.graph.list.delete({ repo: currentAccount!.did, - rkey: new AtUri(initialListUri).rkey, + // TODO list is required, so shouldn't need this assertion + rkey: new AtUri(currentStarterPack.list!.uri).rkey, }) await agent.app.bsky.graph.starterpack.delete({ repo: currentAccount!.did, - rkey, + rkey: new AtUri(currentStarterPack.uri).rkey, }) - setTimeout(async () => { - await invalidateQueries() - logEvent('starterPack:delete', {}) - navigation.popToTop() - }, 2000) + logEvent('starterPack:delete', {}) + navigation.popToTop() } catch (e) { Toast.show(_(msg`Failed to delete starter pack`)) } finally { @@ -456,7 +351,7 @@ function WizardInner({ {state.currentStep === 'Details' ? ( diff --git a/src/state/queries/starter-pack.ts b/src/state/queries/starter-pack.ts index 4f700295d9..81d9adc8b2 100644 --- a/src/state/queries/starter-pack.ts +++ b/src/state/queries/starter-pack.ts @@ -2,7 +2,8 @@ import { AppBskyActorDefs, AppBskyFeedDefs, AppBskyGraphDefs, - AppBskyGraphGetList, + AppBskyGraphStarterpack, + AtUri, BskyAgent, } from '@atproto/api' import {StarterPackView} from '@atproto/api/dist/client/types/app/bsky/graph/defs' @@ -19,10 +20,8 @@ import { httpStarterPackUriToAtUri, parseStarterPackUri, } from 'lib/strings/starter-pack' -import { - invalidateActorStarterPacksQuery, - RQKEY, -} from 'state/queries/actor-starter-packs' +import {invalidateActorStarterPacksQuery} from 'state/queries/actor-starter-packs' +import {invalidateListMembersQuery} from 'state/queries/list-members' import {useAgent} from 'state/session' const RQKEY_ROOT = 'starter-pack' @@ -86,8 +85,9 @@ interface UseCreateStarterPackMutationParams { export function useCreateStarterPackMutation({ onSuccess, + onError, }: { - onSuccess: () => void + onSuccess: (data: {uri: string; cid: string}) => void onError: (e: Error) => void }) { const queryClient = useQueryClient() @@ -100,7 +100,7 @@ export function useCreateStarterPackMutation({ >({ mutationFn: async params => { let listRes - const listRes = await createStarterPackList({...params, agent}) + listRes = await createStarterPackList({...params, agent}) return await agent.app.bsky.graph.starterpack.create( { repo: agent.session?.did, @@ -108,22 +108,134 @@ export function useCreateStarterPackMutation({ }, { ...params, - list: listRes.uri, + list: listRes?.uri, createdAt: new Date().toISOString(), }, ) }, onSuccess: async data => { - await whenAppViewReady( - agent, - data.uri, - (v?: AppBskyGraphDefs.StarterPackView) => { - return typeof v.uri === 'string' - }, - ) + await whenAppViewReady(agent, data.uri, v => { + return typeof v?.uri === 'string' + }) await invalidateActorStarterPacksQuery({ queryClient, - did: agent.session?.did, + did: agent.session!.did, + }) + onSuccess(data) + }, + onError: async error => { + onError(error) + }, + }) +} + +export function useEditStarterPackMutation({ + onSuccess, + onError, +}: { + onSuccess: () => void + onError: (error: Error) => void +}) { + const queryClient = useQueryClient() + const agent = useAgent() + + return useMutation< + void, + Error, + UseCreateStarterPackMutationParams & { + currentStarterPack: AppBskyGraphDefs.StarterPackView + currentListItems: AppBskyGraphDefs.ListItemView[] + } + >({ + mutationFn: async params => { + const { + name, + description, + descriptionFacets, + feeds, + profiles, + currentStarterPack, + currentListItems, + } = params + + if (!AppBskyGraphStarterpack.isRecord(currentStarterPack.record)) { + throw new Error('Invalid starter pack') + } + + const removedItems = currentListItems.filter( + i => + !profiles.find( + p => p.did === i.subject.did && p.did !== agent.session!.did, + ), + ) + + if (removedItems.length !== 0) { + await agent.com.atproto.repo.applyWrites({ + repo: agent.session!.did, + writes: removedItems.map(i => ({ + $type: 'com.atproto.repo.applyWrites#delete', + collection: 'app.bsky.graph.listitem', + rkey: new AtUri(i.uri).rkey, + })), + }) + } + + const addedProfiles = profiles.filter( + p => !currentListItems.find(i => i.subject.did === p.did), + ) + + if (addedProfiles.length > 0) { + await agent.com.atproto.repo.applyWrites({ + repo: agent.session!.did, + writes: addedProfiles.map(p => ({ + $type: 'com.atproto.repo.applyWrites#create', + collection: 'app.bsky.graph.listitem', + value: { + $type: 'app.bsky.graph.listitem', + subject: p.did, + list: currentStarterPack.list?.uri, + createdAt: new Date().toISOString(), + }, + })), + }) + } + + const rkey = parseStarterPackUri(currentStarterPack.uri)!.rkey + await agent.com.atproto.repo.putRecord({ + repo: agent.session!.did, + collection: 'app.bsky.graph.starterpack', + rkey, + record: { + name, + description, + descriptionFacets, + list: currentStarterPack.list?.uri, + feeds, + createdAt: currentStarterPack.record.createdAt, + updatedAt: new Date().toISOString(), + }, + validate: false, // TODO remove! + }) + }, + onSuccess: async (_, {currentStarterPack}) => { + const parsed = parseStarterPackUri(currentStarterPack.uri) + await whenAppViewReady(agent, currentStarterPack.uri, v => { + return currentStarterPack.cid !== v?.cid + }) + await invalidateActorStarterPacksQuery({ + queryClient, + did: agent.session!.did, + }) + if (currentStarterPack.list) { + await invalidateListMembersQuery({ + queryClient, + uri: currentStarterPack.list.uri, + }) + } + await invalidateStarterPack({ + queryClient, + did: agent.session!.did, + rkey: parsed!.rkey, }) onSuccess() }, @@ -136,11 +248,11 @@ export function useCreateStarterPackMutation({ async function whenAppViewReady( agent: BskyAgent, uri: string, - fn: (res?: AppBskyGraphGetList.Response) => boolean, + fn: (res?: AppBskyGraphDefs.StarterPackView) => boolean, ) { await until( - 5, // 5 tries - 1e3, // 1s delay between tries + 10, // 5 tries + 1e5, // 1s delay between tries fn, () => agent.app.bsky.graph.getStarterPack({