diff options
Diffstat (limited to 'src/state')
-rw-r--r-- | src/state/persisted/schema.ts | 2 | ||||
-rw-r--r-- | src/state/preferences/index.tsx | 5 | ||||
-rw-r--r-- | src/state/preferences/used-starter-packs.tsx | 37 | ||||
-rw-r--r-- | src/state/queries/actor-search.ts | 46 | ||||
-rw-r--r-- | src/state/queries/actor-starter-packs.ts | 47 | ||||
-rw-r--r-- | src/state/queries/feed.ts | 17 | ||||
-rw-r--r-- | src/state/queries/list-members.ts | 19 | ||||
-rw-r--r-- | src/state/queries/notifications/feed.ts | 11 | ||||
-rw-r--r-- | src/state/queries/notifications/types.ts | 49 | ||||
-rw-r--r-- | src/state/queries/notifications/util.ts | 83 | ||||
-rw-r--r-- | src/state/queries/profile-lists.ts | 10 | ||||
-rw-r--r-- | src/state/queries/shorten-link.ts | 23 | ||||
-rw-r--r-- | src/state/queries/starter-packs.ts | 317 | ||||
-rw-r--r-- | src/state/session/agent.ts | 12 | ||||
-rw-r--r-- | src/state/shell/logged-out.tsx | 17 | ||||
-rw-r--r-- | src/state/shell/starter-pack.tsx | 25 |
16 files changed, 659 insertions, 61 deletions
diff --git a/src/state/persisted/schema.ts b/src/state/persisted/schema.ts index c942828f2..88fc370a6 100644 --- a/src/state/persisted/schema.ts +++ b/src/state/persisted/schema.ts @@ -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, } diff --git a/src/state/preferences/index.tsx b/src/state/preferences/index.tsx index e1a35f193..e6b53d5be 100644 --- a/src/state/preferences/index.tsx +++ b/src/state/preferences/index.tsx @@ -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> diff --git a/src/state/preferences/used-starter-packs.tsx b/src/state/preferences/used-starter-packs.tsx new file mode 100644 index 000000000..8d5d9e828 --- /dev/null +++ b/src/state/preferences/used-starter-packs.tsx @@ -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) diff --git a/src/state/queries/actor-search.ts b/src/state/queries/actor-search.ts index 1e301a1ba..479fc1a9f 100644 --- a/src/state/queries/actor-search.ts +++ b/src/state/queries/actor-search.ts @@ -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, diff --git a/src/state/queries/actor-starter-packs.ts b/src/state/queries/actor-starter-packs.ts new file mode 100644 index 000000000..9de80b07d --- /dev/null +++ b/src/state/queries/actor-starter-packs.ts @@ -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)}) +} diff --git a/src/state/queries/feed.ts b/src/state/queries/feed.ts index e5d615177..dea6f5d77 100644 --- a/src/state/queries/feed.ts +++ b/src/state/queries/feed.ts @@ -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, diff --git a/src/state/queries/list-members.ts b/src/state/queries/list-members.ts index de9a36ab7..3131a2ec3 100644 --- a/src/state/queries/list-members.ts +++ b/src/state/queries/list-members.ts @@ -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, diff --git a/src/state/queries/notifications/feed.ts b/src/state/queries/notifications/feed.ts index 0607f07a1..13ca3ffde 100644 --- a/src/state/queries/notifications/feed.ts +++ b/src/state/queries/notifications/feed.ts @@ -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) diff --git a/src/state/queries/notifications/types.ts b/src/state/queries/notifications/types.ts index 812236cf0..d40a07b12 100644 --- a/src/state/queries/notifications/types.ts +++ b/src/state/queries/notifications/types.ts @@ -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 +} diff --git a/src/state/queries/notifications/util.ts b/src/state/queries/notifications/util.ts index 8ed1c0390..ade98b317 100644 --- a/src/state/queries/notifications/util.ts +++ b/src/state/queries/notifications/util.ts @@ -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) + } + } + for (const pack of packsChunks.flat()) { + if (AppBskyGraphStarterpack.isRecord(pack.record)) { + packsMap.set(pack.uri, pack) } } - return map + 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 } diff --git a/src/state/queries/profile-lists.ts b/src/state/queries/profile-lists.ts index 2bb5f4d28..112a62c83 100644 --- a/src/state/queries/profile-lists.ts +++ b/src/state/queries/profile-lists.ts @@ -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, diff --git a/src/state/queries/shorten-link.ts b/src/state/queries/shorten-link.ts new file mode 100644 index 000000000..76c63c356 --- /dev/null +++ b/src/state/queries/shorten-link.ts @@ -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() + } +} diff --git a/src/state/queries/starter-packs.ts b/src/state/queries/starter-packs.ts new file mode 100644 index 000000000..241bc6419 --- /dev/null +++ b/src/state/queries/starter-packs.ts @@ -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}), + ) +} diff --git a/src/state/session/agent.ts b/src/state/session/agent.ts index 5a58937fa..4bcb4c11c 100644 --- a/src/state/session/agent.ts +++ b/src/state/session/agent.ts @@ -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. diff --git a/src/state/shell/logged-out.tsx b/src/state/shell/logged-out.tsx index 8fe2a9c01..dc78d03d5 100644 --- a/src/state/shell/logged-out.tsx +++ b/src/state/shell/logged-out.tsx @@ -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>( diff --git a/src/state/shell/starter-pack.tsx b/src/state/shell/starter-pack.tsx new file mode 100644 index 000000000..f564712f0 --- /dev/null +++ b/src/state/shell/starter-pack.tsx @@ -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) |