Feed creation
This commit is contained in:
@@ -61,7 +61,7 @@ function reducer(state: State, action: Action): State {
|
|||||||
|
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case 'SetName':
|
case 'SetName':
|
||||||
updatedState = {...state, name: action.name.slice(0, 50)}
|
updatedState = {...state, name: action.name.slice(0, 24)}
|
||||||
break
|
break
|
||||||
case 'SetDescription':
|
case 'SetDescription':
|
||||||
updatedState = {...state, description: action.description}
|
updatedState = {...state, description: action.description}
|
||||||
|
|||||||
@@ -5,12 +5,12 @@ import {useLingui} from '@lingui/react'
|
|||||||
|
|
||||||
import {useProfileQuery} from '#/state/queries/profile'
|
import {useProfileQuery} from '#/state/queries/profile'
|
||||||
import {useSession} from '#/state/session'
|
import {useSession} from '#/state/session'
|
||||||
import {useWizardState} from '#/screens/StarterPack/Wizard/State'
|
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
import * as TextField from '#/components/forms/TextField'
|
import * as TextField from '#/components/forms/TextField'
|
||||||
import {StarterPack} from '#/components/icons/StarterPack'
|
import {StarterPack} from '#/components/icons/StarterPack'
|
||||||
import {ScreenTransition} from '#/components/StarterPack/Wizard/ScreenTransition'
|
import {ScreenTransition} from '#/components/StarterPack/Wizard/ScreenTransition'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
|
import {useWizardState} from './State'
|
||||||
|
|
||||||
export function StepDetails() {
|
export function StepDetails() {
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
@@ -41,17 +41,13 @@ export function StepDetails() {
|
|||||||
</TextField.LabelText>
|
</TextField.LabelText>
|
||||||
<TextField.Root>
|
<TextField.Root>
|
||||||
<TextField.Input
|
<TextField.Input
|
||||||
label={_(
|
label={_(`My feed`)}
|
||||||
msg`${
|
|
||||||
currentProfile?.displayName || currentProfile?.handle
|
|
||||||
}'s feed`,
|
|
||||||
)}
|
|
||||||
value={state.name}
|
value={state.name}
|
||||||
onChangeText={text => dispatch({type: 'SetName', name: text})}
|
onChangeText={text => dispatch({type: 'SetName', name: text})}
|
||||||
/>
|
/>
|
||||||
<TextField.SuffixText label={_(`${state.name?.length} out of 50`)}>
|
<TextField.SuffixText label={_(`${state.name?.length} out of 24`)}>
|
||||||
<Text style={[t.atoms.text_contrast_medium]}>
|
<Text style={[t.atoms.text_contrast_medium]}>
|
||||||
{state.name?.length ?? 0}/50
|
{state.name?.length ?? 0}/24
|
||||||
</Text>
|
</Text>
|
||||||
</TextField.SuffixText>
|
</TextField.SuffixText>
|
||||||
</TextField.Root>
|
</TextField.Root>
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import {
|
|||||||
KeyboardAwareScrollView,
|
KeyboardAwareScrollView,
|
||||||
useKeyboardController,
|
useKeyboardController,
|
||||||
} from 'react-native-keyboard-controller'
|
} from 'react-native-keyboard-controller'
|
||||||
import {Image} from 'expo-image'
|
|
||||||
import {AppBskyGraphDefs, AtUri} from '@atproto/api'
|
import {AppBskyGraphDefs, AtUri} from '@atproto/api'
|
||||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||||
import {msg, Trans} from '@lingui/macro'
|
import {msg, Trans} from '@lingui/macro'
|
||||||
@@ -13,20 +12,15 @@ import {useFocusEffect, useNavigation} from '@react-navigation/native'
|
|||||||
import {NativeStackScreenProps} from '@react-navigation/native-stack'
|
import {NativeStackScreenProps} from '@react-navigation/native-stack'
|
||||||
|
|
||||||
import {HITSLOP_10} from '#/lib/constants'
|
import {HITSLOP_10} from '#/lib/constants'
|
||||||
import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name'
|
|
||||||
import {CommonNavigatorParams, NavigationProp} from '#/lib/routes/types'
|
import {CommonNavigatorParams, NavigationProp} from '#/lib/routes/types'
|
||||||
import {logEvent} from '#/lib/statsig/statsig'
|
import {parseStarterPackUri} from '#/lib/strings/starter-pack'
|
||||||
import {
|
|
||||||
getStarterPackOgCard,
|
|
||||||
parseStarterPackUri,
|
|
||||||
} from '#/lib/strings/starter-pack'
|
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {isAndroid, isWeb} from '#/platform/detection'
|
import {isAndroid, isWeb} from '#/platform/detection'
|
||||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||||
|
import {useCreateFeedMutation} from '#/state/queries/feed'
|
||||||
import {useAllListMembersQuery} from '#/state/queries/list-members'
|
import {useAllListMembersQuery} from '#/state/queries/list-members'
|
||||||
import {useProfileQuery} from '#/state/queries/profile'
|
import {useProfileQuery} from '#/state/queries/profile'
|
||||||
import {
|
import {
|
||||||
useCreateStarterPackMutation,
|
|
||||||
useEditStarterPackMutation,
|
useEditStarterPackMutation,
|
||||||
useStarterPackQuery,
|
useStarterPackQuery,
|
||||||
} from '#/state/queries/starter-packs'
|
} from '#/state/queries/starter-packs'
|
||||||
@@ -127,10 +121,6 @@ function WizardInner({
|
|||||||
const {setEnabled} = useKeyboardController()
|
const {setEnabled} = useKeyboardController()
|
||||||
const [state, dispatch] = useWizardState()
|
const [state, dispatch] = useWizardState()
|
||||||
const {currentAccount} = useSession()
|
const {currentAccount} = useSession()
|
||||||
const {data: currentProfile} = useProfileQuery({
|
|
||||||
did: currentAccount?.did,
|
|
||||||
staleTime: 0,
|
|
||||||
})
|
|
||||||
const parsed = parseStarterPackUri(currentStarterPack?.uri)
|
const parsed = parseStarterPackUri(currentStarterPack?.uri)
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
@@ -152,8 +142,7 @@ function WizardInner({
|
|||||||
)
|
)
|
||||||
|
|
||||||
const getDefaultName = () => {
|
const getDefaultName = () => {
|
||||||
const displayName = createSanitizedDisplayName(currentProfile!, true)
|
return _(msg`Your feed name`)
|
||||||
return _(msg`${displayName}'s Starter Pack`).slice(0, 50)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const wizardUiStrings: Record<
|
const wizardUiStrings: Record<
|
||||||
@@ -169,18 +158,10 @@ function WizardInner({
|
|||||||
|
|
||||||
const onSuccessCreate = (data: {uri: string; cid: string}) => {
|
const onSuccessCreate = (data: {uri: string; cid: string}) => {
|
||||||
const rkey = new AtUri(data.uri).rkey
|
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,
|
|
||||||
})
|
|
||||||
Image.prefetch([getStarterPackOgCard(currentProfile!.did, rkey)])
|
|
||||||
dispatch({type: 'SetProcessing', processing: false})
|
dispatch({type: 'SetProcessing', processing: false})
|
||||||
navigation.replace('StarterPack', {
|
navigation.replace('ProfileFeed', {
|
||||||
name: currentAccount!.handle,
|
name: currentAccount!.handle,
|
||||||
rkey,
|
rkey,
|
||||||
new: true,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,14 +169,14 @@ function WizardInner({
|
|||||||
if (navigation.canGoBack()) {
|
if (navigation.canGoBack()) {
|
||||||
navigation.goBack()
|
navigation.goBack()
|
||||||
} else {
|
} else {
|
||||||
navigation.replace('StarterPack', {
|
navigation.replace('ProfileFeed', {
|
||||||
name: currentAccount!.handle,
|
name: currentAccount!.handle,
|
||||||
rkey: parsed!.rkey,
|
rkey: parsed!.rkey,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const {mutate: createStarterPack} = useCreateStarterPackMutation({
|
const {mutate: createFeed} = useCreateFeedMutation({
|
||||||
onSuccess: onSuccessCreate,
|
onSuccess: onSuccessCreate,
|
||||||
onError: e => {
|
onError: e => {
|
||||||
logger.error('Failed to create starter pack', {safeMessage: e})
|
logger.error('Failed to create starter pack', {safeMessage: e})
|
||||||
@@ -203,6 +184,8 @@ function WizardInner({
|
|||||||
Toast.show(_(msg`Failed to create starter pack`), 'xmark')
|
Toast.show(_(msg`Failed to create starter pack`), 'xmark')
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// TODO
|
||||||
const {mutate: editStarterPack} = useEditStarterPackMutation({
|
const {mutate: editStarterPack} = useEditStarterPackMutation({
|
||||||
onSuccess: onSuccessEdit,
|
onSuccess: onSuccessEdit,
|
||||||
onError: e => {
|
onError: e => {
|
||||||
@@ -224,11 +207,9 @@ function WizardInner({
|
|||||||
currentListItems: currentListItems,
|
currentListItems: currentListItems,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
createStarterPack({
|
createFeed({
|
||||||
name: state.name?.trim() || getDefaultName(),
|
name: state.name?.trim() || getDefaultName(),
|
||||||
description: state.description?.trim(),
|
description: state.description?.trim(),
|
||||||
profiles: state.profiles,
|
|
||||||
feeds: state.feeds,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
import {useCallback, useEffect, useMemo, useRef} from 'react'
|
import {useCallback, useEffect, useMemo, useRef} from 'react'
|
||||||
|
import {
|
||||||
|
AppBskyFeedGetFeedGenerator,
|
||||||
|
AppBskyRichtextFacet,
|
||||||
|
BskyAgent,
|
||||||
|
} from '@atproto/api'
|
||||||
import {
|
import {
|
||||||
AppBskyActorDefs,
|
AppBskyActorDefs,
|
||||||
AppBskyFeedDefs,
|
AppBskyFeedDefs,
|
||||||
@@ -19,6 +24,7 @@ import {
|
|||||||
useQueryClient,
|
useQueryClient,
|
||||||
} from '@tanstack/react-query'
|
} from '@tanstack/react-query'
|
||||||
|
|
||||||
|
import {until} from '#/lib/async/until'
|
||||||
import {DISCOVER_FEED_URI, DISCOVER_SAVED_FEED} from '#/lib/constants'
|
import {DISCOVER_FEED_URI, DISCOVER_SAVED_FEED} from '#/lib/constants'
|
||||||
import {sanitizeDisplayName} from '#/lib/strings/display-names'
|
import {sanitizeDisplayName} from '#/lib/strings/display-names'
|
||||||
import {sanitizeHandle} from '#/lib/strings/handles'
|
import {sanitizeHandle} from '#/lib/strings/handles'
|
||||||
@@ -639,3 +645,82 @@ export function precacheFeedFromGeneratorView(
|
|||||||
const hydratedFeed = hydrateFeedGenerator(view)
|
const hydratedFeed = hydrateFeedGenerator(view)
|
||||||
precacheFeed(queryClient, hydratedFeed)
|
precacheFeed(queryClient, hydratedFeed)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- feeds playground ---
|
||||||
|
|
||||||
|
interface UseCreateFeedMutationParams {
|
||||||
|
name: string
|
||||||
|
description?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useCreateFeedMutation({
|
||||||
|
onSuccess,
|
||||||
|
onError,
|
||||||
|
}: {
|
||||||
|
onSuccess: (data: {uri: string; cid: string}) => void
|
||||||
|
onError: (e: Error) => void
|
||||||
|
}) {
|
||||||
|
const agent = useAgent()
|
||||||
|
|
||||||
|
return useMutation<
|
||||||
|
{uri: string; cid: string},
|
||||||
|
Error,
|
||||||
|
UseCreateFeedMutationParams
|
||||||
|
>({
|
||||||
|
mutationFn: async ({name, description}) => {
|
||||||
|
let descriptionFacets: AppBskyRichtextFacet.Main[] | undefined
|
||||||
|
if (description) {
|
||||||
|
const rt = new RichText({text: description})
|
||||||
|
await rt.detectFacets(agent)
|
||||||
|
descriptionFacets = rt.facets
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await agent.api.com.atproto.repo.createRecord({
|
||||||
|
repo: agent.session!.did,
|
||||||
|
collection: 'app.bsky.feed.generator',
|
||||||
|
record: {
|
||||||
|
$type: 'app.bsky.feed.generator',
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
did: 'did:web:feeed.club',
|
||||||
|
displayName: name,
|
||||||
|
description,
|
||||||
|
descriptionFacets,
|
||||||
|
labels: {
|
||||||
|
$type: 'com.atproto.label.defs#selfLabels',
|
||||||
|
values: [{val: 'feeed-club'}],
|
||||||
|
},
|
||||||
|
'$club.feeed.generator': {
|
||||||
|
$type: 'club.feeed.generator',
|
||||||
|
actors: [],
|
||||||
|
handleSuffixes: [],
|
||||||
|
tags: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return res.data
|
||||||
|
},
|
||||||
|
onSuccess: async data => {
|
||||||
|
await whenAppViewReady(agent, data.uri, v => {
|
||||||
|
return typeof v?.data.view.uri === 'string'
|
||||||
|
})
|
||||||
|
// TODO: Invalidate query
|
||||||
|
onSuccess(data)
|
||||||
|
},
|
||||||
|
onError: async error => {
|
||||||
|
onError(error)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function whenAppViewReady(
|
||||||
|
agent: BskyAgent,
|
||||||
|
uri: string,
|
||||||
|
fn: (res?: AppBskyFeedGetFeedGenerator.Response) => boolean,
|
||||||
|
) {
|
||||||
|
await until(
|
||||||
|
5, // 5 tries
|
||||||
|
1e3, // 1s delay between tries
|
||||||
|
fn,
|
||||||
|
() => agent.app.bsky.feed.getFeedGenerator({feed: uri}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user