Merge branch 'main' into app-1934

This commit is contained in:
vineyardbovines
2026-04-07 08:55:56 -04:00
15 changed files with 203 additions and 103 deletions
+60 -9
View File
@@ -431,16 +431,30 @@ yarn intl:compile # Compile translations for runtime
// src/state/queries/profile.ts
import {useQuery, useMutation, useQueryClient} from '@tanstack/react-query'
// Query key pattern
const RQKEY_ROOT = 'profile'
export const RQKEY = (did: string) => [RQKEY_ROOT, did]
import {createQueryKey} from '#/state/queries/util'
// Query hook
/*
* Query key name should match the query hook name for consistency
*/
const profileQueryKeyRoot = 'profile'
/*
* Use object params and createQueryKey helper for better readability and to
* avoid bugs with parameter order or types.
*/
export const createProfileQueryKey = (args: {did: string}) =>
createQueryKey(profileQueryKeyRoot, args)
/*
* Query hook should be named use[Name]Query, where [Name] describes the data
* being fetched. This is not a strict requirement, but it's a helpful
* convention for discoverability
*/
export function useProfileQuery({did}: {did: string}) {
const agent = useAgent()
return useQuery({
queryKey: RQKEY(did),
queryKey: createProfileQueryKey({did}),
queryFn: async () => {
const res = await agent.getProfile({actor: did})
return res.data
@@ -450,8 +464,12 @@ export function useProfileQuery({did}: {did: string}) {
})
}
// Mutation hook
export function useUpdateProfile() {
/*
* Mutation hook should match the name of the query hook, but with "Mutation"
* suffix. This is not a strict requirement, but it's a helpful convention for
* discoverability and consistency.
*/
export function useProfileMutation() {
const queryClient = useQueryClient()
return useMutation({
@@ -459,7 +477,9 @@ export function useUpdateProfile() {
// Update logic
},
onSuccess: (_, variables) => {
queryClient.invalidateQueries({queryKey: RQKEY(variables.did)})
queryClient.invalidateQueries({
queryKey: createProfileQueryKey({did: variables.did}),
})
},
onError: (error) => {
if (isNetworkError(error)) {
@@ -473,6 +493,24 @@ export function useUpdateProfile() {
}
})
}
/*
* If cache mutation is needed, include specific interfaces for the specific
* mutations you require adjacent to the source queries. Naming should be
* descriptive of the mutation's purpose, e.g. use[Name]CacheMutation. This is
* not a strict requirement, but it's a helpful convention for discoverability
* and consistency.
*/
export function useProfileCacheMutation() {
const queryClient = useQueryClient()
return (data: Partial<Profile>) => {
queryClient.setQueryData(createProfileQueryKey({did: data.did}), oldData => {
if (!oldData) return oldData
return {...oldData, ...data}
})
}
}
```
**Stale Time Constants** (from `src/state/queries/index.ts`):
@@ -491,7 +529,7 @@ export function useDraftsQuery() {
const agent = useAgent()
return useInfiniteQuery({
queryKey: ['drafts'],
queryKey: createQueryKey('drafts'),
queryFn: async ({pageParam}) => {
const res = await agent.app.bsky.draft.getDrafts({cursor: pageParam})
return res.data
@@ -504,6 +542,19 @@ export function useDraftsQuery() {
To get all items from pages: `data?.pages.flatMap(page => page.items) ?? []`
**Persisted Queries**
To persist query data across app restarts, `createQueryKey` supports a third
parameter called `options`, which has a `persistedVersion` property. When this
property is set to a number, the query will be persisted.
When this property is updated (e.g. incremented), the persisted data will be cleared and replaced with the new data from the query function. This is useful for cases where the shape of the data has changed and old persisted data would no longer be valid.
```tsx
export const createProfileQueryKey = (args: {did: string}) =>
createQueryKey(profileQueryKeyRoot, args, {persistedVersion: 1})
```
### Preferences (React Context)
```tsx
+1 -1
View File
@@ -167,7 +167,7 @@
"expo-location": "~19.0.8",
"expo-media-library": "~18.2.1",
"expo-notifications": "~0.32.16",
"expo-paste-input": "^0.1.10",
"expo-paste-input": "^0.1.12",
"expo-privacy-sensitive": "^0.1.0",
"expo-screen-orientation": "~9.0.8",
"expo-sharing": "~14.0.8",
@@ -46,6 +46,7 @@ export const PostsList = forwardRef<SectionRef, ProfilesListProps>(
return (
<View>
<PostFeed
enabled
feed={feed}
pollInterval={60e3}
scrollElRef={scrollElRef}
+2 -4
View File
@@ -1,7 +1,5 @@
import React from 'react'
export function Provider({children}: React.PropsWithChildren<unknown>) {
return <>{children}</>
export function Provider({children}: {children: React.ReactNode}) {
return children
}
export function useHotkeysContext() {
-1
View File
@@ -1,4 +1,3 @@
import React from 'react'
import {useLingui} from '@lingui/react/macro'
import {
HotkeysProvider,
+2 -3
View File
@@ -10,7 +10,7 @@ import {
import {createPersistedQueryStorage} from '#/lib/persisted-query-storage'
import {listenNetworkConfirmed, listenNetworkLost} from '#/state/events'
import {PERSISTED_QUERY_ROOT} from '#/state/queries'
import {isQueryPersisted} from '#/state/queries/util'
import * as env from '#/env'
import {IS_NATIVE, IS_WEB} from '#/env'
@@ -137,8 +137,7 @@ const dehydrateOptions: PersistQueryClientProviderProps['persistOptions']['dehyd
{
shouldDehydrateMutation: (_: any) => false,
shouldDehydrateQuery: query => {
const root = String(query.queryKey[0])
return root === PERSISTED_QUERY_ROOT
return isQueryPersisted(query.queryKey)
},
}
+30 -30
View File
@@ -910,8 +910,8 @@ msgstr ""
msgid "Add media to post"
msgstr ""
#: src/components/moderation/ReportDialog/index.tsx:522
#: src/components/moderation/ReportDialog/index.tsx:526
#: src/components/moderation/ReportDialog/index.tsx:532
#: src/components/moderation/ReportDialog/index.tsx:536
msgid "Add more details (optional)"
msgstr ""
@@ -1002,7 +1002,7 @@ msgstr ""
msgid "Additional details (limit 1000 characters)"
msgstr ""
#: src/components/moderation/ReportDialog/index.tsx:540
#: src/components/moderation/ReportDialog/index.tsx:550
msgid "Additional details (limit 300 characters)"
msgstr ""
@@ -1887,7 +1887,7 @@ msgstr ""
#. placeholder {0}: sanitizeHandle(item.feed.creator.handle, '@')
#. placeholder {0}: sanitizeHandle(labeler.creator.handle, '@')
#: src/components/LabelingServiceCard/index.tsx:62
#: src/components/moderation/ReportDialog/index.tsx:843
#: src/components/moderation/ReportDialog/index.tsx:853
#: src/screens/Search/components/StarterPackCard.tsx:107
#: src/screens/Search/Explore.tsx:969
msgid "By {0}"
@@ -2028,7 +2028,7 @@ msgstr ""
msgid "Change Handle"
msgstr ""
#: src/components/moderation/ReportDialog/index.tsx:439
#: src/components/moderation/ReportDialog/index.tsx:449
msgid "Change moderation service"
msgstr ""
@@ -2041,11 +2041,11 @@ msgstr ""
msgid "Change password dialog"
msgstr ""
#: src/components/moderation/ReportDialog/index.tsx:304
#: src/components/moderation/ReportDialog/index.tsx:314
msgid "Change report category"
msgstr ""
#: src/components/moderation/ReportDialog/index.tsx:384
#: src/components/moderation/ReportDialog/index.tsx:394
msgid "Change report reason"
msgstr ""
@@ -2430,7 +2430,7 @@ msgstr ""
msgid "Complete the challenge"
msgstr ""
#: src/lib/hotkeys/index.tsx:67
#: src/lib/hotkeys/index.tsx:66
#: src/view/com/feeds/ComposerPrompt.tsx:147
#: src/view/shell/desktop/LeftNav.tsx:575
msgid "Compose new post"
@@ -2919,8 +2919,8 @@ msgstr ""
#. Accessibility label for button to create a moderation report for the selected option
#. placeholder {0}: option.title
#: src/components/moderation/ReportDialog/index.tsx:703
#: src/components/moderation/ReportDialog/index.tsx:749
#: src/components/moderation/ReportDialog/index.tsx:713
#: src/components/moderation/ReportDialog/index.tsx:759
msgid "Create report for {0}"
msgstr ""
@@ -4207,7 +4207,7 @@ msgstr ""
#. placeholder {0}: sanitizeHandle(feed.creatorHandle, '@')
#. placeholder {0}: sanitizeHandle(view.creator.handle, '@')
#: src/components/FeedCard.tsx:170
#: src/state/queries/feed.ts:121
#: src/state/queries/feed.ts:118
#: src/view/com/feeds/FeedSourceCard.tsx:151
msgid "Feed by {0}"
msgstr ""
@@ -4394,7 +4394,7 @@ msgstr ""
msgid "Focus code input"
msgstr ""
#: src/lib/hotkeys/index.tsx:74
#: src/lib/hotkeys/index.tsx:73
msgid "Focus the search field"
msgstr "Focus the search field"
@@ -5943,7 +5943,7 @@ msgstr ""
msgid "Load new notifications"
msgstr ""
#: src/screens/Profile/ProfileFeed/index.tsx:203
#: src/screens/Profile/ProfileFeed/index.tsx:204
#: src/screens/Profile/Sections/Feed.tsx:117
#: src/screens/ProfileList/FeedSection.tsx:113
#: src/view/com/feeds/FeedPage.tsx:170
@@ -6393,8 +6393,8 @@ msgstr ""
msgid "Navigates to your profile"
msgstr ""
#: src/components/moderation/ReportDialog/index.tsx:335
#: src/components/moderation/ReportDialog/index.tsx:352
#: src/components/moderation/ReportDialog/index.tsx:345
#: src/components/moderation/ReportDialog/index.tsx:362
msgid "Need to report a copyright violation, legal request, or regulatory compliance issue?"
msgstr ""
@@ -6480,7 +6480,7 @@ msgstr ""
msgid "New password"
msgstr ""
#: src/screens/Profile/ProfileFeed/index.tsx:220
#: src/screens/Profile/ProfileFeed/index.tsx:221
#: src/screens/ProfileList/index.tsx:251
#: src/screens/ProfileList/index.tsx:300
#: src/view/screens/Feeds.tsx:553
@@ -8339,7 +8339,7 @@ msgid "Report conversation"
msgstr ""
#: src/components/moderation/ReportDialog/index.tsx:98
#: src/components/moderation/ReportDialog/index.tsx:255
#: src/components/moderation/ReportDialog/index.tsx:265
msgid "Report dialog"
msgstr ""
@@ -8554,7 +8554,7 @@ msgstr ""
#: src/components/dms/MessageItem.tsx:322
#: src/components/Error.tsx:66
#: src/components/Lists.tsx:115
#: src/components/moderation/ReportDialog/index.tsx:289
#: src/components/moderation/ReportDialog/index.tsx:299
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoFallback.tsx:56
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoFallback.tsx:59
#: src/components/StarterPack/ProfileStarterPacks.tsx:377
@@ -8576,7 +8576,7 @@ msgstr ""
msgid "Retry"
msgstr ""
#: src/components/moderation/ReportDialog/index.tsx:286
#: src/components/moderation/ReportDialog/index.tsx:296
#: src/view/screens/Storybook/Admonitions.tsx:61
msgid "Retry loading report options"
msgstr ""
@@ -8916,7 +8916,7 @@ msgstr ""
msgid "Select a color"
msgstr ""
#: src/components/moderation/ReportDialog/index.tsx:374
#: src/components/moderation/ReportDialog/index.tsx:384
msgid "Select a reason"
msgstr ""
@@ -9001,7 +9001,7 @@ msgstr ""
msgid "Select languages"
msgstr ""
#: src/components/moderation/ReportDialog/index.tsx:424
#: src/components/moderation/ReportDialog/index.tsx:434
msgid "Select moderation service"
msgstr ""
@@ -9100,7 +9100,7 @@ msgstr ""
msgid "Send post to..."
msgstr ""
#: src/components/moderation/ReportDialog/index.tsx:814
#: src/components/moderation/ReportDialog/index.tsx:824
msgid "Send report to {title}"
msgstr ""
@@ -9603,7 +9603,7 @@ msgid "Something went wrong"
msgstr ""
#: src/components/ageAssurance/AgeAssuranceInitDialog.tsx:139
#: src/components/moderation/ReportDialog/index.tsx:281
#: src/components/moderation/ReportDialog/index.tsx:291
#: src/screens/Deactivated.tsx:86
#: src/screens/Settings/components/DeactivateAccountDialog.tsx:59
#: src/view/screens/Storybook/Admonitions.tsx:56
@@ -9788,9 +9788,9 @@ msgstr ""
msgid "Submit Appeal"
msgstr ""
#: src/components/moderation/ReportDialog/index.tsx:502
#: src/components/moderation/ReportDialog/index.tsx:563
#: src/components/moderation/ReportDialog/index.tsx:570
#: src/components/moderation/ReportDialog/index.tsx:512
#: src/components/moderation/ReportDialog/index.tsx:573
#: src/components/moderation/ReportDialog/index.tsx:580
msgid "Submit report"
msgstr ""
@@ -10185,7 +10185,7 @@ msgid "There was an issue fetching notifications. Tap here to try again."
msgstr ""
#: src/screens/Search/Explore.tsx:1025
#: src/view/com/posts/PostFeed.tsx:763
#: src/view/com/posts/PostFeed.tsx:773
msgid "There was an issue fetching posts. Tap here to try again."
msgstr ""
@@ -10857,7 +10857,7 @@ msgstr ""
msgid "Unfollows the user"
msgstr ""
#: src/components/moderation/ReportDialog/index.tsx:486
#: src/components/moderation/ReportDialog/index.tsx:496
msgid "Unfortunately, none of your subscribed labelers supports this report type."
msgstr ""
@@ -11154,7 +11154,7 @@ msgid "User list by {0}"
msgstr ""
#. placeholder {0}: sanitizeHandle(view.creator.handle, '@')
#: src/state/queries/feed.ts:162
#: src/state/queries/feed.ts:159
msgid "User List by {0}"
msgstr ""
@@ -12540,7 +12540,7 @@ msgid "Your reply was sent"
msgstr ""
#. placeholder {0}: state.selectedLabeler?.creator.displayName
#: src/components/moderation/ReportDialog/index.tsx:513
#: src/components/moderation/ReportDialog/index.tsx:523
msgid "Your report will be sent to <0>{0}</0>."
msgstr ""
@@ -185,6 +185,7 @@ export function ProfileFeedScreenInner({
<FeedFeedbackProvider value={feedFeedback}>
<PostFeed
enabled
feed={feed}
feedParams={feedParams}
pollInterval={60e3}
+18 -11
View File
@@ -22,13 +22,10 @@ import {
import {DISCOVER_FEED_URI, DISCOVER_SAVED_FEED} from '#/lib/constants'
import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {sanitizeHandle} from '#/lib/strings/handles'
import {
PERSISTED_QUERY_GCTIME,
PERSISTED_QUERY_ROOT,
STALE,
} from '#/state/queries'
import {GCTIME, STALE} from '#/state/queries'
import {RQKEY as listQueryKey} from '#/state/queries/list'
import {usePreferencesQuery} from '#/state/queries/preferences'
import {createQueryKey} from '#/state/queries/util'
import {useAgent, useSession} from '#/state/session'
import {router} from '#/routes'
import {useModerationOpts} from '../preferences/moderation-opts'
@@ -416,10 +413,20 @@ const PWI_DISCOVER_FEED_STUB: SavedFeedSourceInfo = {
contentMode: undefined,
}
const createPinnedFeedInfosQueryKeyRoot = (
const createPinnedFeedInfosQueryKey = (
kind: 'pinned' | 'saved',
feedUris: string[],
) => [PERSISTED_QUERY_ROOT, 'feed-info', kind, feedUris]
) =>
createQueryKey(
'feed-info',
{
kind,
feedUris,
},
{
persistedVersion: 1,
},
)
export function usePinnedFeedsInfos() {
const {hasSession} = useSession()
@@ -428,11 +435,11 @@ export function usePinnedFeedsInfos() {
const pinnedItems = preferences?.savedFeeds.filter(feed => feed.pinned) ?? []
return useQuery({
queryKey: createPinnedFeedInfosQueryKeyRoot(
queryKey: createPinnedFeedInfosQueryKey(
'pinned',
pinnedItems.map(f => f.value),
),
gcTime: PERSISTED_QUERY_GCTIME,
gcTime: GCTIME.INFINITY,
staleTime: STALE.INFINITY,
enabled: !isLoadingPrefs,
queryFn: async () => {
@@ -536,11 +543,11 @@ export function useSavedFeeds() {
const queryClient = useQueryClient()
return useQuery({
queryKey: createPinnedFeedInfosQueryKeyRoot(
queryKey: createPinnedFeedInfosQueryKey(
'saved',
savedItems.map(f => f.value),
),
gcTime: PERSISTED_QUERY_GCTIME,
gcTime: GCTIME.INFINITY,
staleTime: STALE.INFINITY,
enabled: !isLoadingPrefs,
placeholderData: previousData => {
+3 -19
View File
@@ -19,22 +19,6 @@ export const STALE = {
INFINITY: Infinity,
}
/**
* Root key for persisted queries.
*
* If the `querykey` of your query uses this at index 0, it will be
* persisted automatically by the `PersistQueryClientProvider` in
* `#/lib/react-query.tsx`.
*
* Be careful when using this, since it will change the query key and may
* break any cases where we call `invalidateQueries` or `refetchQueries`
* with the old key.
*
* Also, only use this for queries that are safe to persist between
* app launches (like user preferences).
*
* Note that for queries that are persisted, it is recommended to extend
* the `gcTime` to a longer duration, otherwise it'll get busted
*/
export const PERSISTED_QUERY_ROOT = 'PERSISTED'
export const PERSISTED_QUERY_GCTIME = Infinity
export const GCTIME = {
INFINITY: Infinity,
}
+6 -12
View File
@@ -3,15 +3,12 @@ import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'
import {z} from 'zod'
import {MAX_LABELERS} from '#/lib/constants'
import {
PERSISTED_QUERY_GCTIME,
PERSISTED_QUERY_ROOT,
STALE,
} from '#/state/queries'
import {GCTIME, STALE} from '#/state/queries'
import {
preferencesQueryKey,
usePreferencesQuery,
} from '#/state/queries/preferences'
import {createQueryKey} from '#/state/queries/util'
import {useAgent} from '#/state/session'
const labelerInfoQueryKeyRoot = 'labeler-info'
@@ -26,11 +23,8 @@ export const labelersInfoQueryKey = (dids: string[]) => [
dids.slice().sort(),
]
const persistedLabelersDetailedInfoQueryKey = (dids: string[]) => [
PERSISTED_QUERY_ROOT,
'labelers-detailed-info',
dids,
]
const createLabelersDetailedInfoQueryKey = (dids: string[]) =>
createQueryKey('labelers-detailed-info', {dids}, {persistedVersion: 1})
export function useLabelerInfoQuery({
did,
@@ -69,8 +63,8 @@ export function useLabelersDetailedInfoQuery({dids}: {dids: string[]}) {
const agent = useAgent()
return useQuery({
enabled: !!dids.length,
queryKey: persistedLabelersDetailedInfoQueryKey(dids),
gcTime: PERSISTED_QUERY_GCTIME,
queryKey: createLabelersDetailedInfoQueryKey(dids),
gcTime: GCTIME.INFINITY,
staleTime: STALE.MINUTES.ONE,
queryFn: async () => {
const res = await agent.app.bsky.labeler.getServices({
+8 -7
View File
@@ -9,11 +9,7 @@ import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'
import {PROD_DEFAULT_FEED} from '#/lib/constants'
import {replaceEqualDeep} from '#/lib/functions'
import {getAge} from '#/lib/strings/time'
import {
PERSISTED_QUERY_GCTIME,
PERSISTED_QUERY_ROOT,
STALE,
} from '#/state/queries'
import {GCTIME, STALE} from '#/state/queries'
import {
DEFAULT_HOME_FEED_PREFS,
DEFAULT_LOGGED_OUT_PREFERENCES,
@@ -23,6 +19,7 @@ import {
type ThreadViewPreferences,
type UsePreferencesQueryResponse,
} from '#/state/queries/preferences/types'
import {createQueryKey} from '#/state/queries/util'
import {useAgent} from '#/state/session'
import {saveLabelers} from '#/state/session/agent-config'
import {useAgeAssurance} from '#/ageAssurance'
@@ -33,7 +30,11 @@ export * from '#/state/queries/preferences/const'
export * from '#/state/queries/preferences/moderation'
export * from '#/state/queries/preferences/types'
export const preferencesQueryKey = [PERSISTED_QUERY_ROOT, 'getPreferences']
export const preferencesQueryKey = createQueryKey(
'getPreferences',
{},
{persistedVersion: 1},
)
export function usePreferencesQuery() {
const agent = useAgent()
@@ -44,7 +45,7 @@ export function usePreferencesQuery() {
structuralSharing: replaceEqualDeep,
refetchOnWindowFocus: true,
queryKey: preferencesQueryKey,
gcTime: PERSISTED_QUERY_GCTIME,
gcTime: GCTIME.INFINITY,
queryFn: async () => {
if (!agent.did) {
return DEFAULT_LOGGED_OUT_PREFERENCES
+55
View File
@@ -14,6 +14,61 @@ import {
import * as bsky from '#/types/bsky'
export type StructuredQueryKey<T extends Record<string, unknown>> = readonly [
string,
T,
{
persistedVersion?: number
},
]
/**
* Helper method to ensure consistent query keys and key ordering
*/
export function createQueryKey<T extends Record<string, unknown>>(
/**
* The query key root. All queries must have a root.
*/
root: string,
/**
* Any arguments the query depends on, and if changed, should result in the query being refetched.
*/
args: T,
options: {
/**
* If provided, this indicates that the query is persisted and the version
* of the persisted query format.
*
* This is used to ensure that when we make breaking changes to the
* persisted query format, we can increment the version and avoid trying to
* read old persisted queries with the new format.
*
* If you're persisting your queries, you probably want to set `gcTime:
* GCTIME.INFINITY` for this query, otherwise it'll get busted immediately
* after being persisted.
*/
persistedVersion?: number
} = {},
): StructuredQueryKey<T> {
return [root, args, options] as const
}
export function isQueryPersisted(
queryKey: QueryKey,
): queryKey is StructuredQueryKey<Record<string, unknown>> {
return (
Array.isArray(queryKey) &&
queryKey.length === 3 &&
typeof queryKey[0] === 'string' &&
typeof queryKey[1] === 'object' &&
queryKey[1] !== null &&
typeof queryKey[2] === 'object' &&
queryKey[2] !== null &&
'persistedVersion' in queryKey[2] &&
typeof queryKey[2].persistedVersion === 'number'
)
}
export async function truncateAndInvalidate<T = any>(
queryClient: QueryClient,
queryKey: QueryKey,
+12 -2
View File
@@ -38,6 +38,7 @@ import {
RQKEY,
usePostFeedQuery,
} from '#/state/queries/post-feed'
import {truncateAndInvalidate} from '#/state/queries/util'
import {useSession} from '#/state/session'
import {useProgressGuide} from '#/state/shell/progress-guide'
import {useSelectedFeed} from '#/state/shell/selected-feed'
@@ -700,13 +701,22 @@ let PostFeed = ({
})
setIsPTRing(true)
try {
await refetch()
await truncateAndInvalidate(queryClient, RQKEY(feed, feedParams))
onHasNew?.(false)
} catch (err) {
logger.error('Failed to refresh posts feed', {message: err})
}
setIsPTRing(false)
}, [ax, refetch, setIsPTRing, onHasNew, feed, feedType, enabled])
}, [
ax,
queryClient,
setIsPTRing,
onHasNew,
feed,
feedParams,
feedType,
enabled,
])
const onEndReached = useCallback(async () => {
if (isFetching || !hasNextPage || isError) return
+4 -4
View File
@@ -9086,10 +9086,10 @@ expo-notifications@~0.32.16:
expo-application "~7.0.8"
expo-constants "~18.0.13"
expo-paste-input@^0.1.10:
version "0.1.10"
resolved "https://registry.yarnpkg.com/expo-paste-input/-/expo-paste-input-0.1.10.tgz#7df0a07ae6ecd381004624dbe88762997d962780"
integrity sha512-TkAauK1eIq7+vk2SA39trmDO6dLfFLQL+09KIVZh//3XWs8gHC5ezH3vjfjr7xj9xi67amyeiDCErP5caTDIKg==
expo-paste-input@^0.1.12:
version "0.1.12"
resolved "https://registry.yarnpkg.com/expo-paste-input/-/expo-paste-input-0.1.12.tgz#0295eb26caf738fc39019f4ecae48c22b126b67e"
integrity sha512-iaOCgygmCYVbSj+gJOxr8P28y8Tf0X4UHtVVXRjEsARKf5gQUoVaRX63uzB+8h+g6vN710fSmu5vdxNP28bw8g==
expo-privacy-sensitive@^0.1.0:
version "0.1.0"