Merge remote-tracking branch 'upstream/main' into make-more-localizable

This commit is contained in:
Minseo Lee
2024-05-13 10:31:09 +09:00
16 changed files with 332 additions and 159 deletions
+1 -1
View File
@@ -51,7 +51,7 @@
},
"dependencies": {
"@atproto-labs/api": "^0.12.8-clipclops.0",
"@atproto/api": "^0.12.6",
"@atproto/api": "^0.12.9",
"@bam.tech/react-native-image-resizer": "^3.0.4",
"@braintree/sanitize-url": "^6.0.2",
"@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet",
+1 -3
View File
@@ -32,9 +32,7 @@ export function LabelsOnMe({
if (!labels || !currentAccount) {
return null
}
labels = labels.filter(
l => !l.val.startsWith('!') && l.src !== currentAccount.did,
)
labels = labels.filter(l => !l.val.startsWith('!'))
if (!labels.length) {
return null
}
+61 -35
View File
@@ -7,7 +7,7 @@ import {useLingui} from '@lingui/react'
import {useLabelInfo} from '#/lib/moderation/useLabelInfo'
import {makeProfileLink} from '#/lib/routes/links'
import {sanitizeHandle} from '#/lib/strings/handles'
import {useAgent} from '#/state/session'
import {useAgent, useSession} from '#/state/session'
import * as Toast from '#/view/com/util/Toast'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {Button, ButtonText} from '#/components/Button'
@@ -33,13 +33,28 @@ export interface LabelsOnMeDialogProps {
labels: ComAtprotoLabelDefs.Label[]
}
export function LabelsOnMeDialogInner(props: LabelsOnMeDialogProps) {
export function LabelsOnMeDialog(props: LabelsOnMeDialogProps) {
return (
<Dialog.Outer control={props.control}>
<Dialog.Handle />
<LabelsOnMeDialogInner {...props} />
</Dialog.Outer>
)
}
function LabelsOnMeDialogInner(props: LabelsOnMeDialogProps) {
const {_} = useLingui()
const {currentAccount} = useSession()
const [appealingLabel, setAppealingLabel] = React.useState<
ComAtprotoLabelDefs.Label | undefined
>(undefined)
const {subject, labels} = props
const isAccount = 'did' in subject
const containsSelfLabel = React.useMemo(
() => labels.some(l => l.src === currentAccount?.did),
[currentAccount?.did, labels],
)
return (
<Dialog.ScrollableInner
@@ -65,9 +80,17 @@ export function LabelsOnMeDialogInner(props: LabelsOnMeDialogProps) {
)}
</Text>
<Text style={[a.text_md, a.leading_snug]}>
<Trans>
You may appeal these labels if you feel they were placed in error.
</Trans>
{containsSelfLabel ? (
<Trans>
You may appeal non-self labels if you feel they were placed in
error.
</Trans>
) : (
<Trans>
You may appeal these labels if you feel they were placed in
error.
</Trans>
)}
</Text>
<View style={[a.py_lg, a.gap_md]}>
@@ -75,6 +98,7 @@ export function LabelsOnMeDialogInner(props: LabelsOnMeDialogProps) {
<Label
key={`${label.val}-${label.src}`}
label={label}
isSelfLabel={label.src === currentAccount?.did}
control={props.control}
onPressAppeal={label => setAppealingLabel(label)}
/>
@@ -88,22 +112,14 @@ export function LabelsOnMeDialogInner(props: LabelsOnMeDialogProps) {
)
}
export function LabelsOnMeDialog(props: LabelsOnMeDialogProps) {
return (
<Dialog.Outer control={props.control}>
<Dialog.Handle />
<LabelsOnMeDialogInner {...props} />
</Dialog.Outer>
)
}
function Label({
label,
isSelfLabel,
control,
onPressAppeal,
}: {
label: ComAtprotoLabelDefs.Label
isSelfLabel: boolean
control: Dialog.DialogOuterProps['control']
onPressAppeal: (label: ComAtprotoLabelDefs.Label) => void
}) {
@@ -125,32 +141,42 @@ function Label({
{strings.description}
</Text>
</View>
<View>
<Button
variant="solid"
color="secondary"
size="small"
label={_(msg`Appeal`)}
onPress={() => onPressAppeal(label)}>
<ButtonText>
<Trans>Appeal</Trans>
</ButtonText>
</Button>
</View>
{!isSelfLabel && (
<View>
<Button
variant="solid"
color="secondary"
size="small"
label={_(msg`Appeal`)}
onPress={() => onPressAppeal(label)}>
<ButtonText>
<Trans>Appeal</Trans>
</ButtonText>
</Button>
</View>
)}
</View>
<Divider />
<View style={[a.px_md, a.py_sm, t.atoms.bg_contrast_25]}>
<Text style={[t.atoms.text_contrast_medium]}>
<Trans>Source:</Trans>{' '}
<InlineLinkText
to={makeProfileLink(
labeler ? labeler.creator : {did: label.src, handle: ''},
)}
onPress={() => control.close()}>
{labeler ? sanitizeHandle(labeler.creator.handle, '@') : label.src}
</InlineLinkText>
{isSelfLabel ? (
<Trans>This label was applied by you</Trans>
) : (
<>
<Trans>Source:</Trans>{' '}
<InlineLinkText
to={makeProfileLink(
labeler ? labeler.creator : {did: label.src, handle: ''},
)}
onPress={() => control.close()}>
{labeler
? sanitizeHandle(labeler.creator.handle, '@')
: label.src}
</InlineLinkText>
</>
)}
</Text>
</View>
</View>
+4 -2
View File
@@ -1,5 +1,5 @@
import {BskyAgent, stringifyLex, jsonToLex} from '@atproto/api'
import RNFS from 'react-native-fs'
import {BskyAgent, jsonToLex, stringifyLex} from '@atproto/api'
const GET_TIMEOUT = 15e3 // 15s
const POST_TIMEOUT = 60e3 // 60s
@@ -68,8 +68,10 @@ async function fetchHandler(
resBody = jsonToLex(await res.json())
} else if (resMimeType.startsWith('text/')) {
resBody = await res.text()
} else if (resMimeType === 'application/vnd.ipld.car') {
resBody = await res.arrayBuffer()
} else {
throw new Error('TODO: non-textual response body')
throw new Error('Non-supported mime type')
}
}
+4
View File
@@ -71,6 +71,10 @@ export class FeedViewPostsSlice {
?.__source as ReasonFeedSource
}
get feedContext() {
return this.items.find(item => item.feedContext)?.feedContext
}
containsUri(uri: string) {
return !!this.items.find(item => item.post.uri === uri)
}
+73 -1
View File
@@ -1,12 +1,23 @@
import {Image as RNImage, Share as RNShare} from 'react-native'
import {Image} from 'react-native-image-crop-picker'
import uuid from 'react-native-uuid'
import {cacheDirectory, copyAsync, deleteAsync} from 'expo-file-system'
import {
cacheDirectory,
copyAsync,
deleteAsync,
documentDirectory,
EncodingType,
makeDirectoryAsync,
StorageAccessFramework,
writeAsStringAsync,
} from 'expo-file-system'
import * as MediaLibrary from 'expo-media-library'
import * as Sharing from 'expo-sharing'
import ImageResizer from '@bam.tech/react-native-image-resizer'
import {Buffer} from 'buffer'
import RNFetchBlob from 'rn-fetch-blob'
import {logger} from '#/logger'
import {isAndroid, isIOS} from 'platform/detection'
import {Dimensions} from './types'
@@ -240,3 +251,64 @@ function normalizePath(str: string, allPlatforms = false): string {
}
return str
}
export async function saveBytesToDisk(
filename: string,
bytes: Uint8Array,
type: string,
) {
const encoded = Buffer.from(bytes).toString('base64')
return await saveToDevice(filename, encoded, type)
}
export async function saveToDevice(
filename: string,
encoded: string,
type: string,
) {
try {
if (isIOS) {
const tmpFileUrl = await withTempFile(filename, encoded)
await Sharing.shareAsync(tmpFileUrl, {UTI: type})
safeDeleteAsync(tmpFileUrl)
return true
} else {
const permissions =
await StorageAccessFramework.requestDirectoryPermissionsAsync()
if (!permissions.granted) {
return false
}
const fileUrl = await StorageAccessFramework.createFileAsync(
permissions.directoryUri,
filename,
type,
)
await writeAsStringAsync(fileUrl, encoded, {
encoding: EncodingType.Base64,
})
return true
}
} catch (e) {
logger.error('Error occurred while saving file', {message: e})
return false
}
}
async function withTempFile(
filename: string,
encoded: string,
): Promise<string> {
// Using a directory so that the file name is not a random string
// documentDirectory will always be available on native, so we assert as a string.
const tmpDirUri = joinPath(documentDirectory as string, String(uuid.v4()))
await makeDirectoryAsync(tmpDirUri, {intermediates: true})
const tmpFileUrl = joinPath(tmpDirUri, filename)
await writeAsStringAsync(tmpFileUrl, encoded, {
encoding: EncodingType.Base64,
})
return tmpFileUrl
}
+23 -2
View File
@@ -1,6 +1,7 @@
import {Dimensions} from './types'
import {Image as RNImage} from 'react-native-image-crop-picker'
import {getDataUriSize, blobToDataUri} from './util'
import {Dimensions} from './types'
import {blobToDataUri, getDataUriSize} from './util'
export async function compressIfNeeded(
img: RNImage,
@@ -138,3 +139,23 @@ function createResizedImage(
img.src = dataUri
})
}
export async function saveBytesToDisk(
filename: string,
bytes: Uint8Array,
type: string,
) {
const blob = new Blob([bytes], {type})
const url = URL.createObjectURL(blob)
await downloadUrl(url, filename)
// Firefox requires a small delay
setTimeout(() => URL.revokeObjectURL(url), 100)
return true
}
async function downloadUrl(href: string, filename: string) {
const a = document.createElement('a')
a.href = href
a.download = filename
a.click()
}
+35 -20
View File
@@ -3,13 +3,18 @@ import {View} from 'react-native'
import {TID} from '@atproto/common-web'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useQueryClient} from '@tanstack/react-query'
import {useAnalytics} from '#/lib/analytics/analytics'
import {BSKY_APP_ACCOUNT_DID, IS_PROD_SERVICE} from '#/lib/constants'
import {DISCOVER_SAVED_FEED, TIMELINE_SAVED_FEED} from '#/lib/constants'
import {logEvent, useGate} from '#/lib/statsig/statsig'
import {logger} from '#/logger'
import {useOverwriteSavedFeedsMutation} from '#/state/queries/preferences'
import {
preferencesQueryKey,
useOverwriteSavedFeedsMutation,
} from '#/state/queries/preferences'
import {RQKEY as profileRQKey} from '#/state/queries/profile'
import {useAgent} from '#/state/session'
import {useOnboardingDispatch} from '#/state/shell'
import {uploadBlob} from 'lib/api'
@@ -41,6 +46,7 @@ export function StepFinished() {
const onboardDispatch = useOnboardingDispatch()
const [saving, setSaving] = React.useState(false)
const {mutateAsync: overwriteSavedFeeds} = useOverwriteSavedFeedsMutation()
const queryClient = useQueryClient()
const {getAgent} = useAgent()
const gate = useGate()
@@ -112,33 +118,41 @@ export function StepFinished() {
])
}
})(),
])
if (gate('reduced_onboarding_and_home_algo')) {
await getAgent().upsertProfile(async existing => {
existing = existing ?? {}
if (profileStepResults.imageUri && profileStepResults.imageMime) {
const res = await uploadBlob(
getAgent(),
profileStepResults.imageUri,
profileStepResults.imageMime,
)
if (res.data.blob) {
existing.avatar = res.data.blob
}
(async () => {
const {imageUri, imageMime} = profileStepResults
if (imageUri && imageMime) {
const blobPromise = uploadBlob(getAgent(), imageUri, imageMime)
await getAgent().upsertProfile(async existing => {
existing = existing ?? {}
const res = await blobPromise
if (res.data.blob) {
existing.avatar = res.data.blob
}
return existing
})
}
return existing
})
}
})(),
])
} catch (e: any) {
logger.info(`onboarding: bulk save failed`)
logger.error(e)
// don't alert the user, just let them into their account
}
// Try to ensure that prefs and profile are up-to-date by the time we render Home.
await Promise.all([
queryClient.invalidateQueries({
queryKey: preferencesQueryKey,
}),
queryClient.invalidateQueries({
queryKey: profileRQKey(getAgent().session?.did ?? ''),
}),
]).catch(e => {
logger.error(e)
// Keep going.
})
setSaving(false)
dispatch({type: 'finish'})
onboardDispatch({type: 'finish'})
@@ -154,6 +168,7 @@ export function StepFinished() {
track,
getAgent,
gate,
queryClient,
])
React.useEffect(() => {
+1 -1
View File
@@ -303,7 +303,7 @@ export function usePostFeedQuery(
i === 0 && slice.source
? slice.source
: item.reason,
feedContext: item.feedContext,
feedContext: item.feedContext || slice.feedContext,
moderation: moderations[i],
}
}
+40
View File
@@ -6,6 +6,7 @@ import {
import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'
import {track} from '#/lib/analytics/analytics'
import {PROD_DEFAULT_FEED} from '#/lib/constants'
import {replaceEqualDeep} from '#/lib/functions'
import {getAge} from '#/lib/strings/time'
import {STALE} from '#/state/queries'
@@ -244,6 +245,45 @@ export function useRemoveFeedMutation() {
})
}
export function useReplaceForYouWithDiscoverFeedMutation() {
const queryClient = useQueryClient()
const {getAgent} = useAgent()
return useMutation({
mutationFn: async ({
forYouFeedConfig,
discoverFeedConfig,
}: {
forYouFeedConfig: AppBskyActorDefs.SavedFeed | undefined
discoverFeedConfig: AppBskyActorDefs.SavedFeed | undefined
}) => {
if (forYouFeedConfig) {
await getAgent().removeSavedFeeds([forYouFeedConfig.id])
}
if (!discoverFeedConfig) {
await getAgent().addSavedFeeds([
{
type: 'feed',
value: PROD_DEFAULT_FEED('whats-hot'),
pinned: true,
},
])
} else {
await getAgent().updateSavedFeeds([
{
...discoverFeedConfig,
pinned: true,
},
])
}
// triggers a refetch
await queryClient.invalidateQueries({
queryKey: preferencesQueryKey,
})
},
})
}
export function useUpdateSavedFeedsMutation() {
const queryClient = useQueryClient()
const {getAgent} = useAgent()
+25 -12
View File
@@ -6,6 +6,7 @@ import {
AppBskyActorProfile,
AtUri,
BskyAgent,
ComAtprotoRepoUploadBlob,
} from '@atproto/api'
import {
QueryClient,
@@ -124,6 +125,26 @@ export function useProfileUpdateMutation() {
newUserBanner,
checkCommitted,
}) => {
let newUserAvatarPromise:
| Promise<ComAtprotoRepoUploadBlob.Response>
| undefined
if (newUserAvatar) {
newUserAvatarPromise = uploadBlob(
getAgent(),
newUserAvatar.path,
newUserAvatar.mime,
)
}
let newUserBannerPromise:
| Promise<ComAtprotoRepoUploadBlob.Response>
| undefined
if (newUserBanner) {
newUserBannerPromise = uploadBlob(
getAgent(),
newUserBanner.path,
newUserBanner.mime,
)
}
await getAgent().upsertProfile(async existing => {
existing = existing || {}
if (typeof updates === 'function') {
@@ -132,22 +153,14 @@ export function useProfileUpdateMutation() {
existing.displayName = updates.displayName
existing.description = updates.description
}
if (newUserAvatar) {
const res = await uploadBlob(
getAgent(),
newUserAvatar.path,
newUserAvatar.mime,
)
if (newUserAvatarPromise) {
const res = await newUserAvatarPromise
existing.avatar = res.data.blob
} else if (newUserAvatar === null) {
existing.avatar = undefined
}
if (newUserBanner) {
const res = await uploadBlob(
getAgent(),
newUserBanner.path,
newUserBanner.mime,
)
if (newUserBannerPromise) {
const res = await newUserBannerPromise
existing.banner = res.data.blob
} else if (newUserBanner === null) {
existing.banner = undefined
+14 -34
View File
@@ -6,10 +6,9 @@ import {useLingui} from '@lingui/react'
import {PROD_DEFAULT_FEED} from '#/lib/constants'
import {logger} from '#/logger'
import {
useAddSavedFeedsMutation,
usePreferencesQuery,
useRemoveFeedMutation,
useUpdateSavedFeedsMutation,
useReplaceForYouWithDiscoverFeedMutation,
} from '#/state/queries/preferences'
import {useSetSelectedFeed} from '#/state/shell/selected-feed'
import * as Toast from '#/view/com/util/Toast'
@@ -24,12 +23,10 @@ export function FeedShutdownMsg({feedUri}: {feedUri: string}) {
const {_} = useLingui()
const setSelectedFeed = useSetSelectedFeed()
const {data: preferences} = usePreferencesQuery()
const {mutateAsync: addSavedFeeds, isPending: isAddSavedFeedPending} =
useAddSavedFeedsMutation()
const {mutateAsync: removeFeed, isPending: isRemovePending} =
useRemoveFeedMutation()
const {mutateAsync: updateSavedFeeds, isPending: isUpdateFeedPending} =
useUpdateSavedFeedsMutation()
const {mutateAsync: replaceFeedWithDiscover, isPending: isReplacePending} =
useReplaceForYouWithDiscoverFeedMutation()
const feedConfig = preferences?.savedFeeds?.find(
f => f.value === feedUri && f.pinned,
@@ -46,6 +43,9 @@ export function FeedShutdownMsg({feedUri}: {feedUri: string}) {
await removeFeed(feedConfig)
Toast.show(_(msg`Removed from your feeds`))
}
if (hasDiscoverPinned) {
setSelectedFeed(`feedgen|${PROD_DEFAULT_FEED('whats-hot')}`)
}
} catch (err: any) {
Toast.show(
_(
@@ -54,30 +54,15 @@ export function FeedShutdownMsg({feedUri}: {feedUri: string}) {
)
logger.error('Failed up update feeds', {message: err})
}
}, [removeFeed, feedConfig, _])
}, [removeFeed, feedConfig, _, hasDiscoverPinned, setSelectedFeed])
const onReplaceFeed = React.useCallback(async () => {
try {
if (!discoverFeedConfig) {
await addSavedFeeds([
{
type: 'feed',
value: PROD_DEFAULT_FEED('whats-hot'),
pinned: true,
},
])
} else {
await updateSavedFeeds([
{
...discoverFeedConfig,
pinned: true,
},
])
}
await replaceFeedWithDiscover({
forYouFeedConfig: feedConfig,
discoverFeedConfig,
})
setSelectedFeed(`feedgen|${PROD_DEFAULT_FEED('whats-hot')}`)
if (feedConfig) {
await removeFeed(feedConfig)
}
Toast.show(_(msg`The feed has been replaced with Discover.`))
} catch (err: any) {
Toast.show(
@@ -88,17 +73,14 @@ export function FeedShutdownMsg({feedUri}: {feedUri: string}) {
logger.error('Failed up update feeds', {message: err})
}
}, [
addSavedFeeds,
updateSavedFeeds,
removeFeed,
replaceFeedWithDiscover,
discoverFeedConfig,
feedConfig,
setSelectedFeed,
_,
])
const isProcessing =
isAddSavedFeedPending || isUpdateFeedPending || isRemovePending
const isProcessing = isReplacePending || isRemovePending
return (
<View
style={[
@@ -147,9 +129,7 @@ export function FeedShutdownMsg({feedUri}: {feedUri: string}) {
<ButtonText>
<Trans>Replace with Discover</Trans>
</ButtonText>
{(isAddSavedFeedPending || isUpdateFeedPending) && (
<ButtonIcon icon={Loader} />
)}
{isReplacePending && <ButtonIcon icon={Loader} />}
</Button>
)}
</View>
+1 -1
View File
@@ -90,7 +90,7 @@ function ListImpl<ItemT>(
},
{
itemVisiblePercentThreshold: 40,
minimumViewTime: 2e3,
minimumViewTime: 1.5e3,
},
]
}, [onItemSeen])
+1 -1
View File
@@ -27,7 +27,7 @@ export type ListProps<ItemT> = Omit<
}
export type ListRef = React.MutableRefObject<any | null> // TODO: Better types.
const ON_ITEM_SEEN_WAIT_DURATION = 2e3 // post must be "seen" 2 seconds before capturing
const ON_ITEM_SEEN_WAIT_DURATION = 1.5e3 // when we consider post to be "seen"
const ON_ITEM_SEEN_INTERSECTION_OPTS = {
rootMargin: '-200px 0px -200px 0px',
} // post must be 200px visible to be "seen"
+44 -42
View File
@@ -3,12 +3,16 @@ import {View} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useAgent, useSession} from '#/state/session'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {Button, ButtonText} from '#/components/Button'
import {saveBytesToDisk} from '#/lib/media/manip'
import {logger} from '#/logger'
import {useAgent} from '#/state/session'
import * as Toast from '#/view/com/util/Toast'
import {atoms as a, useTheme} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
import {InlineLinkText, Link} from '#/components/Link'
import {P, Text} from '#/components/Typography'
import {InlineLinkText} from '#/components/Link'
import {Loader} from '#/components/Loader'
import {Text} from '#/components/Typography'
export function ExportCarDialog({
control,
@@ -17,21 +21,35 @@ export function ExportCarDialog({
}) {
const {_} = useLingui()
const t = useTheme()
const {gtMobile} = useBreakpoints()
const {currentAccount} = useSession()
const {getAgent} = useAgent()
const [loading, setLoading] = React.useState(false)
const downloadUrl = React.useMemo(() => {
const download = React.useCallback(async () => {
const agent = getAgent()
if (!currentAccount || !agent.session) {
return '' // shouldnt ever happen
if (!agent.session) {
return // shouldnt ever happen
}
// eg: https://bsky.social/xrpc/com.atproto.sync.getRepo?did=did:plc:ewvi7nxzyoun6zhxrhs64oiz
const url = new URL(agent.pdsUrl || agent.service)
url.pathname = '/xrpc/com.atproto.sync.getRepo'
url.searchParams.set('did', agent.session.did)
return url.toString()
}, [currentAccount, getAgent])
try {
setLoading(true)
const did = agent.session.did
const downloadRes = await agent.com.atproto.sync.getRepo({did})
const saveRes = await saveBytesToDisk(
'repo.car',
downloadRes.data,
downloadRes.headers['content-type'],
)
if (saveRes) {
Toast.show(_(msg`File saved successfully!`))
}
} catch (e) {
logger.error('Error occurred while downloading CAR file', {message: e})
Toast.show(_(msg`Error occurred while saving file`))
} finally {
setLoading(false)
control.close()
}
}, [_, control, getAgent])
return (
<Dialog.Outer control={control}>
@@ -40,34 +58,34 @@ export function ExportCarDialog({
<Dialog.ScrollableInner
accessibilityDescribedBy="dialog-description"
accessibilityLabelledBy="dialog-title">
<View style={[a.relative, a.gap_md, a.w_full]}>
<View style={[a.relative, a.gap_lg, a.w_full]}>
<Text nativeID="dialog-title" style={[a.text_2xl, a.font_bold]}>
<Trans>Export My Data</Trans>
</Text>
<P nativeID="dialog-description" style={[a.text_sm]}>
<Text nativeID="dialog-description" style={[a.text_sm]}>
<Trans>
Your account repository, containing all public data records, can
be downloaded as a "CAR" file. This file does not include media
embeds, such as images, or your private data, which must be
fetched separately.
</Trans>
</P>
</Text>
<Link
<Button
variant="solid"
color="primary"
size="large"
label={_(msg`Download CAR file`)}
to={downloadUrl}
download="repo.car">
disabled={loading}
onPress={download}>
<ButtonText>
<Trans>Download CAR file</Trans>
</ButtonText>
</Link>
{loading && <ButtonIcon icon={Loader} />}
</Button>
<P
<Text
style={[
a.py_xs,
t.atoms.text_contrast_medium,
a.text_sm,
a.leading_snug,
@@ -83,23 +101,7 @@ export function ExportCarDialog({
</InlineLinkText>
.
</Trans>
</P>
<View style={gtMobile && [a.flex_row, a.justify_end]}>
<Button
testID="doneBtn"
variant="outline"
color="primary"
size={gtMobile ? 'small' : 'large'}
onPress={() => control.close()}
label={_(msg`Done`)}>
<ButtonText>
<Trans>Done</Trans>
</ButtonText>
</Button>
</View>
{!gtMobile && <View style={{height: 40}} />}
</Text>
</View>
</Dialog.ScrollableInner>
</Dialog.Outer>
+4 -4
View File
@@ -58,10 +58,10 @@
multiformats "^9.9.0"
tlds "^1.234.0"
"@atproto/api@^0.12.6":
version "0.12.6"
resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.12.6.tgz#690c004c5ac7fc7bceac4605d8c1ec1f580be270"
integrity sha512-30htXN2Hjl1jzzeAtIhggOsVS4vA975pMUQYoA4xMonug+z6O9NHcka3yYb4C9ldpnGugvRPKH7EhAUbiDTC5w==
"@atproto/api@^0.12.9":
version "0.12.9"
resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.12.9.tgz#5ae040980e574a5d9496368c4ca032c0cda174ec"
integrity sha512-3D4n2ZAAsDRnjevvcoIxQxuMMoqc+7vtVyP7EnrEdeOmRSCF9j8yXTqhn6rcHCbzcs3DKyYR26nQemtZsMsE0g==
dependencies:
"@atproto/common-web" "^0.3.0"
"@atproto/lexicon" "^0.4.0"