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:
Hailey 2024-06-21 21:38:04 -07:00 committed by GitHub
parent 35f64535cb
commit f089f45781
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
115 changed files with 6336 additions and 237 deletions

View file

@ -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,
}

View file

@ -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>

View 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)

View file

@ -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,

View 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)})
}

View file

@ -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,

View file

@ -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,

View file

@ -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)

View file

@ -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
}

View file

@ -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
}

View file

@ -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,

View 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()
}
}

View 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}),
)
}

View file

@ -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.

View file

@ -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>(

View 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)