Starter Packs (#4332)
Co-authored-by: Dan Abramov <dan.abramov@gmail.com> Co-authored-by: Paul Frazee <pfrazee@gmail.com> Co-authored-by: Eric Bailey <git@esb.lol> Co-authored-by: Samuel Newman <mozzius@protonmail.com>
This commit is contained in:
parent
35f64535cb
commit
f089f45781
115 changed files with 6336 additions and 237 deletions
|
@ -88,6 +88,7 @@ export const schema = z.object({
|
|||
disableHaptics: z.boolean().optional(),
|
||||
disableAutoplay: z.boolean().optional(),
|
||||
kawaii: z.boolean().optional(),
|
||||
hasCheckedForStarterPack: z.boolean().optional(),
|
||||
/** @deprecated */
|
||||
mutedThreads: z.array(z.string()),
|
||||
})
|
||||
|
@ -129,4 +130,5 @@ export const defaults: Schema = {
|
|||
disableHaptics: false,
|
||||
disableAutoplay: prefersReducedMotion,
|
||||
kawaii: false,
|
||||
hasCheckedForStarterPack: false,
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import {Provider as InAppBrowserProvider} from './in-app-browser'
|
|||
import {Provider as KawaiiProvider} from './kawaii'
|
||||
import {Provider as LanguagesProvider} from './languages'
|
||||
import {Provider as LargeAltBadgeProvider} from './large-alt-badge'
|
||||
import {Provider as UsedStarterPacksProvider} from './used-starter-packs'
|
||||
|
||||
export {
|
||||
useRequireAltTextEnabled,
|
||||
|
@ -34,7 +35,9 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
|||
<InAppBrowserProvider>
|
||||
<DisableHapticsProvider>
|
||||
<AutoplayProvider>
|
||||
<KawaiiProvider>{children}</KawaiiProvider>
|
||||
<UsedStarterPacksProvider>
|
||||
<KawaiiProvider>{children}</KawaiiProvider>
|
||||
</UsedStarterPacksProvider>
|
||||
</AutoplayProvider>
|
||||
</DisableHapticsProvider>
|
||||
</InAppBrowserProvider>
|
||||
|
|
37
src/state/preferences/used-starter-packs.tsx
Normal file
37
src/state/preferences/used-starter-packs.tsx
Normal file
|
@ -0,0 +1,37 @@
|
|||
import React from 'react'
|
||||
|
||||
import * as persisted from '#/state/persisted'
|
||||
|
||||
type StateContext = boolean | undefined
|
||||
type SetContext = (v: boolean) => void
|
||||
|
||||
const stateContext = React.createContext<StateContext>(false)
|
||||
const setContext = React.createContext<SetContext>((_: boolean) => {})
|
||||
|
||||
export function Provider({children}: {children: React.ReactNode}) {
|
||||
const [state, setState] = React.useState<StateContext>(() =>
|
||||
persisted.get('hasCheckedForStarterPack'),
|
||||
)
|
||||
|
||||
const setStateWrapped = (v: boolean) => {
|
||||
setState(v)
|
||||
persisted.write('hasCheckedForStarterPack', v)
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
return persisted.onUpdate(() => {
|
||||
setState(persisted.get('hasCheckedForStarterPack'))
|
||||
})
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<stateContext.Provider value={state}>
|
||||
<setContext.Provider value={setStateWrapped}>
|
||||
{children}
|
||||
</setContext.Provider>
|
||||
</stateContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useHasCheckedForStarterPack = () => React.useContext(stateContext)
|
||||
export const useSetHasCheckedForStarterPack = () => React.useContext(setContext)
|
|
@ -1,5 +1,11 @@
|
|||
import {AppBskyActorDefs} from '@atproto/api'
|
||||
import {QueryClient, useQuery} from '@tanstack/react-query'
|
||||
import {AppBskyActorDefs, AppBskyActorSearchActors} from '@atproto/api'
|
||||
import {
|
||||
InfiniteData,
|
||||
QueryClient,
|
||||
QueryKey,
|
||||
useInfiniteQuery,
|
||||
useQuery,
|
||||
} from '@tanstack/react-query'
|
||||
|
||||
import {STALE} from '#/state/queries'
|
||||
import {useAgent} from '#/state/session'
|
||||
|
@ -7,6 +13,11 @@ import {useAgent} from '#/state/session'
|
|||
const RQKEY_ROOT = 'actor-search'
|
||||
export const RQKEY = (query: string) => [RQKEY_ROOT, query]
|
||||
|
||||
export const RQKEY_PAGINATED = (query: string) => [
|
||||
`${RQKEY_ROOT}_paginated`,
|
||||
query,
|
||||
]
|
||||
|
||||
export function useActorSearch({
|
||||
query,
|
||||
enabled,
|
||||
|
@ -28,6 +39,37 @@ export function useActorSearch({
|
|||
})
|
||||
}
|
||||
|
||||
export function useActorSearchPaginated({
|
||||
query,
|
||||
enabled,
|
||||
}: {
|
||||
query: string
|
||||
enabled?: boolean
|
||||
}) {
|
||||
const agent = useAgent()
|
||||
return useInfiniteQuery<
|
||||
AppBskyActorSearchActors.OutputSchema,
|
||||
Error,
|
||||
InfiniteData<AppBskyActorSearchActors.OutputSchema>,
|
||||
QueryKey,
|
||||
string | undefined
|
||||
>({
|
||||
staleTime: STALE.MINUTES.FIVE,
|
||||
queryKey: RQKEY_PAGINATED(query),
|
||||
queryFn: async ({pageParam}) => {
|
||||
const res = await agent.searchActors({
|
||||
q: query,
|
||||
limit: 25,
|
||||
cursor: pageParam,
|
||||
})
|
||||
return res.data
|
||||
},
|
||||
enabled: enabled && !!query,
|
||||
initialPageParam: undefined,
|
||||
getNextPageParam: lastPage => lastPage.cursor,
|
||||
})
|
||||
}
|
||||
|
||||
export function* findAllProfilesInQueryData(
|
||||
queryClient: QueryClient,
|
||||
did: string,
|
||||
|
|
47
src/state/queries/actor-starter-packs.ts
Normal file
47
src/state/queries/actor-starter-packs.ts
Normal file
|
@ -0,0 +1,47 @@
|
|||
import {AppBskyGraphGetActorStarterPacks} from '@atproto/api'
|
||||
import {
|
||||
InfiniteData,
|
||||
QueryClient,
|
||||
QueryKey,
|
||||
useInfiniteQuery,
|
||||
} from '@tanstack/react-query'
|
||||
|
||||
import {useAgent} from 'state/session'
|
||||
|
||||
const RQKEY_ROOT = 'actor-starter-packs'
|
||||
export const RQKEY = (did?: string) => [RQKEY_ROOT, did]
|
||||
|
||||
export function useActorStarterPacksQuery({did}: {did?: string}) {
|
||||
const agent = useAgent()
|
||||
|
||||
return useInfiniteQuery<
|
||||
AppBskyGraphGetActorStarterPacks.OutputSchema,
|
||||
Error,
|
||||
InfiniteData<AppBskyGraphGetActorStarterPacks.OutputSchema>,
|
||||
QueryKey,
|
||||
string | undefined
|
||||
>({
|
||||
queryKey: RQKEY(did),
|
||||
queryFn: async ({pageParam}: {pageParam?: string}) => {
|
||||
const res = await agent.app.bsky.graph.getActorStarterPacks({
|
||||
actor: did!,
|
||||
limit: 10,
|
||||
cursor: pageParam,
|
||||
})
|
||||
return res.data
|
||||
},
|
||||
enabled: Boolean(did),
|
||||
initialPageParam: undefined,
|
||||
getNextPageParam: lastPage => lastPage.cursor,
|
||||
})
|
||||
}
|
||||
|
||||
export async function invalidateActorStarterPacksQuery({
|
||||
queryClient,
|
||||
did,
|
||||
}: {
|
||||
queryClient: QueryClient
|
||||
did: string
|
||||
}) {
|
||||
await queryClient.invalidateQueries({queryKey: RQKEY(did)})
|
||||
}
|
|
@ -9,6 +9,7 @@ import {
|
|||
} from '@atproto/api'
|
||||
import {
|
||||
InfiniteData,
|
||||
keepPreviousData,
|
||||
QueryClient,
|
||||
QueryKey,
|
||||
useInfiniteQuery,
|
||||
|
@ -315,6 +316,22 @@ export function useSearchPopularFeedsMutation() {
|
|||
})
|
||||
}
|
||||
|
||||
export function useSearchPopularFeedsQuery({q}: {q: string}) {
|
||||
const agent = useAgent()
|
||||
return useQuery({
|
||||
queryKey: ['searchPopularFeeds', q],
|
||||
queryFn: async () => {
|
||||
const res = await agent.app.bsky.unspecced.getPopularFeedGenerators({
|
||||
limit: 15,
|
||||
query: q,
|
||||
})
|
||||
|
||||
return res.data.feeds
|
||||
},
|
||||
placeholderData: keepPreviousData,
|
||||
})
|
||||
}
|
||||
|
||||
const popularFeedsSearchQueryKeyRoot = 'popularFeedsSearch'
|
||||
export const createPopularFeedsSearchQueryKey = (query: string) => [
|
||||
popularFeedsSearchQueryKeyRoot,
|
||||
|
|
|
@ -15,7 +15,7 @@ type RQPageParam = string | undefined
|
|||
const RQKEY_ROOT = 'list-members'
|
||||
export const RQKEY = (uri: string) => [RQKEY_ROOT, uri]
|
||||
|
||||
export function useListMembersQuery(uri: string) {
|
||||
export function useListMembersQuery(uri?: string, limit: number = PAGE_SIZE) {
|
||||
const agent = useAgent()
|
||||
return useInfiniteQuery<
|
||||
AppBskyGraphGetList.OutputSchema,
|
||||
|
@ -25,20 +25,31 @@ export function useListMembersQuery(uri: string) {
|
|||
RQPageParam
|
||||
>({
|
||||
staleTime: STALE.MINUTES.ONE,
|
||||
queryKey: RQKEY(uri),
|
||||
queryKey: RQKEY(uri ?? ''),
|
||||
async queryFn({pageParam}: {pageParam: RQPageParam}) {
|
||||
const res = await agent.app.bsky.graph.getList({
|
||||
list: uri,
|
||||
limit: PAGE_SIZE,
|
||||
list: uri!, // the enabled flag will prevent this from running until uri is set
|
||||
limit,
|
||||
cursor: pageParam,
|
||||
})
|
||||
return res.data
|
||||
},
|
||||
initialPageParam: undefined,
|
||||
getNextPageParam: lastPage => lastPage.cursor,
|
||||
enabled: Boolean(uri),
|
||||
})
|
||||
}
|
||||
|
||||
export async function invalidateListMembersQuery({
|
||||
queryClient,
|
||||
uri,
|
||||
}: {
|
||||
queryClient: QueryClient
|
||||
uri: string
|
||||
}) {
|
||||
await queryClient.invalidateQueries({queryKey: RQKEY(uri)})
|
||||
}
|
||||
|
||||
export function* findAllProfilesInQueryData(
|
||||
queryClient: QueryClient,
|
||||
did: string,
|
||||
|
|
|
@ -155,8 +155,10 @@ export function* findAllPostsInQueryData(
|
|||
|
||||
for (const page of queryData?.pages) {
|
||||
for (const item of page.items) {
|
||||
if (item.subject && didOrHandleUriMatches(atUri, item.subject)) {
|
||||
yield item.subject
|
||||
if (item.type !== 'starterpack-joined') {
|
||||
if (item.subject && didOrHandleUriMatches(atUri, item.subject)) {
|
||||
yield item.subject
|
||||
}
|
||||
}
|
||||
|
||||
const quotedPost = getEmbeddedPost(item.subject?.embed)
|
||||
|
@ -181,7 +183,10 @@ export function* findAllProfilesInQueryData(
|
|||
}
|
||||
for (const page of queryData?.pages) {
|
||||
for (const item of page.items) {
|
||||
if (item.subject?.author.did === did) {
|
||||
if (
|
||||
item.type !== 'starterpack-joined' &&
|
||||
item.subject?.author.did === did
|
||||
) {
|
||||
yield item.subject.author
|
||||
}
|
||||
const quotedPost = getEmbeddedPost(item.subject?.embed)
|
||||
|
|
|
@ -1,26 +1,22 @@
|
|||
import {
|
||||
AppBskyNotificationListNotifications,
|
||||
AppBskyFeedDefs,
|
||||
AppBskyGraphDefs,
|
||||
AppBskyNotificationListNotifications,
|
||||
} from '@atproto/api'
|
||||
|
||||
export type NotificationType =
|
||||
| 'post-like'
|
||||
| 'feedgen-like'
|
||||
| 'repost'
|
||||
| 'mention'
|
||||
| 'reply'
|
||||
| 'quote'
|
||||
| 'follow'
|
||||
| 'unknown'
|
||||
| StarterPackNotificationType
|
||||
| OtherNotificationType
|
||||
|
||||
export interface FeedNotification {
|
||||
_reactKey: string
|
||||
type: NotificationType
|
||||
notification: AppBskyNotificationListNotifications.Notification
|
||||
additional?: AppBskyNotificationListNotifications.Notification[]
|
||||
subjectUri?: string
|
||||
subject?: AppBskyFeedDefs.PostView
|
||||
}
|
||||
export type FeedNotification =
|
||||
| (FeedNotificationBase & {
|
||||
type: StarterPackNotificationType
|
||||
subject?: AppBskyGraphDefs.StarterPackViewBasic
|
||||
})
|
||||
| (FeedNotificationBase & {
|
||||
type: OtherNotificationType
|
||||
subject?: AppBskyFeedDefs.PostView
|
||||
})
|
||||
|
||||
export interface FeedPage {
|
||||
cursor: string | undefined
|
||||
|
@ -37,3 +33,22 @@ export interface CachedFeedPage {
|
|||
data: FeedPage | undefined
|
||||
unreadCount: number
|
||||
}
|
||||
|
||||
type StarterPackNotificationType = 'starterpack-joined'
|
||||
type OtherNotificationType =
|
||||
| 'post-like'
|
||||
| 'repost'
|
||||
| 'mention'
|
||||
| 'reply'
|
||||
| 'quote'
|
||||
| 'follow'
|
||||
| 'feedgen-like'
|
||||
| 'unknown'
|
||||
|
||||
type FeedNotificationBase = {
|
||||
_reactKey: string
|
||||
notification: AppBskyNotificationListNotifications.Notification
|
||||
additional?: AppBskyNotificationListNotifications.Notification[]
|
||||
subjectUri?: string
|
||||
subject?: AppBskyFeedDefs.PostView | AppBskyGraphDefs.StarterPackViewBasic
|
||||
}
|
||||
|
|
|
@ -3,6 +3,8 @@ import {
|
|||
AppBskyFeedLike,
|
||||
AppBskyFeedPost,
|
||||
AppBskyFeedRepost,
|
||||
AppBskyGraphDefs,
|
||||
AppBskyGraphStarterpack,
|
||||
AppBskyNotificationListNotifications,
|
||||
BskyAgent,
|
||||
moderateNotification,
|
||||
|
@ -40,6 +42,7 @@ export async function fetchPage({
|
|||
limit,
|
||||
cursor,
|
||||
})
|
||||
|
||||
const indexedAt = res.data.notifications[0]?.indexedAt
|
||||
|
||||
// filter out notifs by mod rules
|
||||
|
@ -56,9 +59,18 @@ export async function fetchPage({
|
|||
const subjects = await fetchSubjects(agent, notifsGrouped)
|
||||
for (const notif of notifsGrouped) {
|
||||
if (notif.subjectUri) {
|
||||
notif.subject = subjects.get(notif.subjectUri)
|
||||
if (notif.subject) {
|
||||
precacheProfile(queryClient, notif.subject.author)
|
||||
if (
|
||||
notif.type === 'starterpack-joined' &&
|
||||
notif.notification.reasonSubject
|
||||
) {
|
||||
notif.subject = subjects.starterPacks.get(
|
||||
notif.notification.reasonSubject,
|
||||
)
|
||||
} else {
|
||||
notif.subject = subjects.posts.get(notif.subjectUri)
|
||||
if (notif.subject) {
|
||||
precacheProfile(queryClient, notif.subject.author)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -120,12 +132,21 @@ export function groupNotifications(
|
|||
}
|
||||
if (!grouped) {
|
||||
const type = toKnownType(notif)
|
||||
groupedNotifs.push({
|
||||
_reactKey: `notif-${notif.uri}`,
|
||||
type,
|
||||
notification: notif,
|
||||
subjectUri: getSubjectUri(type, notif),
|
||||
})
|
||||
if (type !== 'starterpack-joined') {
|
||||
groupedNotifs.push({
|
||||
_reactKey: `notif-${notif.uri}`,
|
||||
type,
|
||||
notification: notif,
|
||||
subjectUri: getSubjectUri(type, notif),
|
||||
})
|
||||
} else {
|
||||
groupedNotifs.push({
|
||||
_reactKey: `notif-${notif.uri}`,
|
||||
type: 'starterpack-joined',
|
||||
notification: notif,
|
||||
subjectUri: notif.uri,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return groupedNotifs
|
||||
|
@ -134,29 +155,54 @@ export function groupNotifications(
|
|||
async function fetchSubjects(
|
||||
agent: BskyAgent,
|
||||
groupedNotifs: FeedNotification[],
|
||||
): Promise<Map<string, AppBskyFeedDefs.PostView>> {
|
||||
const uris = new Set<string>()
|
||||
): Promise<{
|
||||
posts: Map<string, AppBskyFeedDefs.PostView>
|
||||
starterPacks: Map<string, AppBskyGraphDefs.StarterPackViewBasic>
|
||||
}> {
|
||||
const postUris = new Set<string>()
|
||||
const packUris = new Set<string>()
|
||||
for (const notif of groupedNotifs) {
|
||||
if (notif.subjectUri?.includes('app.bsky.feed.post')) {
|
||||
uris.add(notif.subjectUri)
|
||||
postUris.add(notif.subjectUri)
|
||||
} else if (
|
||||
notif.notification.reasonSubject?.includes('app.bsky.graph.starterpack')
|
||||
) {
|
||||
packUris.add(notif.notification.reasonSubject)
|
||||
}
|
||||
}
|
||||
const uriChunks = chunk(Array.from(uris), 25)
|
||||
const postUriChunks = chunk(Array.from(postUris), 25)
|
||||
const packUriChunks = chunk(Array.from(packUris), 25)
|
||||
const postsChunks = await Promise.all(
|
||||
uriChunks.map(uris =>
|
||||
postUriChunks.map(uris =>
|
||||
agent.app.bsky.feed.getPosts({uris}).then(res => res.data.posts),
|
||||
),
|
||||
)
|
||||
const map = new Map<string, AppBskyFeedDefs.PostView>()
|
||||
const packsChunks = await Promise.all(
|
||||
packUriChunks.map(uris =>
|
||||
agent.app.bsky.graph
|
||||
.getStarterPacks({uris})
|
||||
.then(res => res.data.starterPacks),
|
||||
),
|
||||
)
|
||||
const postsMap = new Map<string, AppBskyFeedDefs.PostView>()
|
||||
const packsMap = new Map<string, AppBskyGraphDefs.StarterPackView>()
|
||||
for (const post of postsChunks.flat()) {
|
||||
if (
|
||||
AppBskyFeedPost.isRecord(post.record) &&
|
||||
AppBskyFeedPost.validateRecord(post.record).success
|
||||
) {
|
||||
map.set(post.uri, post)
|
||||
postsMap.set(post.uri, post)
|
||||
}
|
||||
}
|
||||
return map
|
||||
for (const pack of packsChunks.flat()) {
|
||||
if (AppBskyGraphStarterpack.isRecord(pack.record)) {
|
||||
packsMap.set(pack.uri, pack)
|
||||
}
|
||||
}
|
||||
return {
|
||||
posts: postsMap,
|
||||
starterPacks: packsMap,
|
||||
}
|
||||
}
|
||||
|
||||
function toKnownType(
|
||||
|
@ -173,7 +219,8 @@ function toKnownType(
|
|||
notif.reason === 'mention' ||
|
||||
notif.reason === 'reply' ||
|
||||
notif.reason === 'quote' ||
|
||||
notif.reason === 'follow'
|
||||
notif.reason === 'follow' ||
|
||||
notif.reason === 'starterpack-joined'
|
||||
) {
|
||||
return notif.reason as NotificationType
|
||||
}
|
||||
|
|
|
@ -26,7 +26,15 @@ export function useProfileListsQuery(did: string, opts?: {enabled?: boolean}) {
|
|||
limit: PAGE_SIZE,
|
||||
cursor: pageParam,
|
||||
})
|
||||
return res.data
|
||||
|
||||
// Starter packs use a reference list, which we do not want to show on profiles. At some point we could probably
|
||||
// just filter this out on the backend instead of in the client.
|
||||
return {
|
||||
...res.data,
|
||||
lists: res.data.lists.filter(
|
||||
l => l.purpose !== 'app.bsky.graph.defs#referencelist',
|
||||
),
|
||||
}
|
||||
},
|
||||
initialPageParam: undefined,
|
||||
getNextPageParam: lastPage => lastPage.cursor,
|
||||
|
|
23
src/state/queries/shorten-link.ts
Normal file
23
src/state/queries/shorten-link.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
import {logger} from '#/logger'
|
||||
|
||||
export function useShortenLink() {
|
||||
return async (inputUrl: string): Promise<{url: string}> => {
|
||||
const url = new URL(inputUrl)
|
||||
const res = await fetch('https://go.bsky.app/link', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
path: url.pathname,
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
if (!res.ok) {
|
||||
logger.error('Failed to shorten link', {safeMessage: res.status})
|
||||
return {url: inputUrl}
|
||||
}
|
||||
|
||||
return res.json()
|
||||
}
|
||||
}
|
317
src/state/queries/starter-packs.ts
Normal file
317
src/state/queries/starter-packs.ts
Normal file
|
@ -0,0 +1,317 @@
|
|||
import {
|
||||
AppBskyActorDefs,
|
||||
AppBskyFeedDefs,
|
||||
AppBskyGraphDefs,
|
||||
AppBskyGraphGetStarterPack,
|
||||
AppBskyGraphStarterpack,
|
||||
AtUri,
|
||||
BskyAgent,
|
||||
} from '@atproto/api'
|
||||
import {StarterPackView} from '@atproto/api/dist/client/types/app/bsky/graph/defs'
|
||||
import {
|
||||
QueryClient,
|
||||
useMutation,
|
||||
useQuery,
|
||||
useQueryClient,
|
||||
} from '@tanstack/react-query'
|
||||
|
||||
import {until} from 'lib/async/until'
|
||||
import {createStarterPackList} from 'lib/generate-starterpack'
|
||||
import {
|
||||
createStarterPackUri,
|
||||
httpStarterPackUriToAtUri,
|
||||
parseStarterPackUri,
|
||||
} from 'lib/strings/starter-pack'
|
||||
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'
|
||||
const RQKEY = (did?: string, rkey?: string) => {
|
||||
if (did?.startsWith('https://') || did?.startsWith('at://')) {
|
||||
const parsed = parseStarterPackUri(did)
|
||||
return [RQKEY_ROOT, parsed?.name, parsed?.rkey]
|
||||
} else {
|
||||
return [RQKEY_ROOT, did, rkey]
|
||||
}
|
||||
}
|
||||
|
||||
export function useStarterPackQuery({
|
||||
uri,
|
||||
did,
|
||||
rkey,
|
||||
}: {
|
||||
uri?: string
|
||||
did?: string
|
||||
rkey?: string
|
||||
}) {
|
||||
const agent = useAgent()
|
||||
|
||||
return useQuery<StarterPackView>({
|
||||
queryKey: RQKEY(did, rkey),
|
||||
queryFn: async () => {
|
||||
if (!uri) {
|
||||
uri = `at://${did}/app.bsky.graph.starterpack/${rkey}`
|
||||
} else if (uri && !uri.startsWith('at://')) {
|
||||
uri = httpStarterPackUriToAtUri(uri) as string
|
||||
}
|
||||
|
||||
const res = await agent.app.bsky.graph.getStarterPack({
|
||||
starterPack: uri,
|
||||
})
|
||||
return res.data.starterPack
|
||||
},
|
||||
enabled: Boolean(uri) || Boolean(did && rkey),
|
||||
})
|
||||
}
|
||||
|
||||
export async function invalidateStarterPack({
|
||||
queryClient,
|
||||
did,
|
||||
rkey,
|
||||
}: {
|
||||
queryClient: QueryClient
|
||||
did: string
|
||||
rkey: string
|
||||
}) {
|
||||
await queryClient.invalidateQueries({queryKey: RQKEY(did, rkey)})
|
||||
}
|
||||
|
||||
interface UseCreateStarterPackMutationParams {
|
||||
name: string
|
||||
description?: string
|
||||
descriptionFacets: []
|
||||
profiles: AppBskyActorDefs.ProfileViewBasic[]
|
||||
feeds?: AppBskyFeedDefs.GeneratorView[]
|
||||
}
|
||||
|
||||
export function useCreateStarterPackMutation({
|
||||
onSuccess,
|
||||
onError,
|
||||
}: {
|
||||
onSuccess: (data: {uri: string; cid: string}) => void
|
||||
onError: (e: Error) => void
|
||||
}) {
|
||||
const queryClient = useQueryClient()
|
||||
const agent = useAgent()
|
||||
|
||||
return useMutation<
|
||||
{uri: string; cid: string},
|
||||
Error,
|
||||
UseCreateStarterPackMutationParams
|
||||
>({
|
||||
mutationFn: async params => {
|
||||
let listRes
|
||||
listRes = await createStarterPackList({...params, agent})
|
||||
return await agent.app.bsky.graph.starterpack.create(
|
||||
{
|
||||
repo: agent.session?.did,
|
||||
},
|
||||
{
|
||||
...params,
|
||||
list: listRes?.uri,
|
||||
createdAt: new Date().toISOString(),
|
||||
},
|
||||
)
|
||||
},
|
||||
onSuccess: async data => {
|
||||
await whenAppViewReady(agent, data.uri, v => {
|
||||
return typeof v?.data.starterPack.uri === 'string'
|
||||
})
|
||||
await invalidateActorStarterPacksQuery({
|
||||
queryClient,
|
||||
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 =>
|
||||
i.subject.did !== agent.session?.did &&
|
||||
!profiles.find(p => p.did === i.subject.did && p.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(),
|
||||
},
|
||||
})
|
||||
},
|
||||
onSuccess: async (_, {currentStarterPack}) => {
|
||||
const parsed = parseStarterPackUri(currentStarterPack.uri)
|
||||
await whenAppViewReady(agent, currentStarterPack.uri, v => {
|
||||
return currentStarterPack.cid !== v?.data.starterPack.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()
|
||||
},
|
||||
onError: error => {
|
||||
onError(error)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function useDeleteStarterPackMutation({
|
||||
onSuccess,
|
||||
onError,
|
||||
}: {
|
||||
onSuccess: () => void
|
||||
onError: (error: Error) => void
|
||||
}) {
|
||||
const agent = useAgent()
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({listUri, rkey}: {listUri?: string; rkey: string}) => {
|
||||
if (!agent.session) {
|
||||
throw new Error(`Requires logged in user`)
|
||||
}
|
||||
|
||||
if (listUri) {
|
||||
await agent.app.bsky.graph.list.delete({
|
||||
repo: agent.session.did,
|
||||
rkey: new AtUri(listUri).rkey,
|
||||
})
|
||||
}
|
||||
await agent.app.bsky.graph.starterpack.delete({
|
||||
repo: agent.session.did,
|
||||
rkey,
|
||||
})
|
||||
},
|
||||
onSuccess: async (_, {listUri, rkey}) => {
|
||||
const uri = createStarterPackUri({
|
||||
did: agent.session!.did,
|
||||
rkey,
|
||||
})
|
||||
|
||||
if (uri) {
|
||||
await whenAppViewReady(agent, uri, v => {
|
||||
return Boolean(v?.data?.starterPack) === false
|
||||
})
|
||||
}
|
||||
|
||||
if (listUri) {
|
||||
await invalidateListMembersQuery({queryClient, uri: listUri})
|
||||
}
|
||||
await invalidateActorStarterPacksQuery({
|
||||
queryClient,
|
||||
did: agent.session!.did,
|
||||
})
|
||||
await invalidateStarterPack({
|
||||
queryClient,
|
||||
did: agent.session!.did,
|
||||
rkey,
|
||||
})
|
||||
onSuccess()
|
||||
},
|
||||
onError: error => {
|
||||
onError(error)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async function whenAppViewReady(
|
||||
agent: BskyAgent,
|
||||
uri: string,
|
||||
fn: (res?: AppBskyGraphGetStarterPack.Response) => boolean,
|
||||
) {
|
||||
await until(
|
||||
5, // 5 tries
|
||||
1e3, // 1s delay between tries
|
||||
fn,
|
||||
() => agent.app.bsky.graph.getStarterPack({starterPack: uri}),
|
||||
)
|
||||
}
|
|
@ -127,18 +127,6 @@ export async function createAgentAndCreateAccount(
|
|||
const account = agentToSessionAccountOrThrow(agent)
|
||||
const gates = tryFetchGates(account.did, 'prefer-fresh-gates')
|
||||
const moderation = configureModerationForAccount(agent, account)
|
||||
if (!account.signupQueued) {
|
||||
/*dont await*/ agent.upsertProfile(_existing => {
|
||||
return {
|
||||
displayName: '',
|
||||
// HACKFIX
|
||||
// creating a bunch of identical profile objects is breaking the relay
|
||||
// tossing this unspecced field onto it to reduce the size of the problem
|
||||
// -prf
|
||||
createdAt: new Date().toISOString(),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Not awaited so that we can still get into onboarding.
|
||||
// This is OK because we won't let you toggle adult stuff until you set the date.
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
import React from 'react'
|
||||
|
||||
import {isWeb} from 'platform/detection'
|
||||
import {useSession} from 'state/session'
|
||||
import {useActiveStarterPack} from 'state/shell/starter-pack'
|
||||
|
||||
type State = {
|
||||
showLoggedOut: boolean
|
||||
/**
|
||||
|
@ -22,7 +26,7 @@ type Controls = {
|
|||
/**
|
||||
* The did of the account to populate the login form with.
|
||||
*/
|
||||
requestedAccount?: string | 'none' | 'new'
|
||||
requestedAccount?: string | 'none' | 'new' | 'starterpack'
|
||||
}) => void
|
||||
/**
|
||||
* Clears the requested account so that next time the logged out view is
|
||||
|
@ -43,9 +47,16 @@ const ControlsContext = React.createContext<Controls>({
|
|||
})
|
||||
|
||||
export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||
const activeStarterPack = useActiveStarterPack()
|
||||
const {hasSession} = useSession()
|
||||
const shouldShowStarterPack = Boolean(activeStarterPack?.uri) && !hasSession
|
||||
const [state, setState] = React.useState<State>({
|
||||
showLoggedOut: false,
|
||||
requestedAccountSwitchTo: undefined,
|
||||
showLoggedOut: shouldShowStarterPack,
|
||||
requestedAccountSwitchTo: shouldShowStarterPack
|
||||
? isWeb
|
||||
? 'starterpack'
|
||||
: 'new'
|
||||
: undefined,
|
||||
})
|
||||
|
||||
const controls = React.useMemo<Controls>(
|
||||
|
|
25
src/state/shell/starter-pack.tsx
Normal file
25
src/state/shell/starter-pack.tsx
Normal file
|
@ -0,0 +1,25 @@
|
|||
import React from 'react'
|
||||
|
||||
type StateContext =
|
||||
| {
|
||||
uri: string
|
||||
isClip?: boolean
|
||||
}
|
||||
| undefined
|
||||
type SetContext = (v: StateContext) => void
|
||||
|
||||
const stateContext = React.createContext<StateContext>(undefined)
|
||||
const setContext = React.createContext<SetContext>((_: StateContext) => {})
|
||||
|
||||
export function Provider({children}: {children: React.ReactNode}) {
|
||||
const [state, setState] = React.useState<StateContext>()
|
||||
|
||||
return (
|
||||
<stateContext.Provider value={state}>
|
||||
<setContext.Provider value={setState}>{children}</setContext.Provider>
|
||||
</stateContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useActiveStarterPack = () => React.useContext(stateContext)
|
||||
export const useSetActiveStarterPack = () => React.useContext(setContext)
|
Loading…
Add table
Add a link
Reference in a new issue