diff options
-rw-r--r-- | src/components/StarterPack/QrCodeDialog.tsx | 8 | ||||
-rw-r--r-- | src/components/StarterPack/ShareDialog.tsx | 28 | ||||
-rw-r--r-- | src/lib/media/save-image.ts | 59 | ||||
-rw-r--r-- | src/view/com/lightbox/Lightbox.tsx | 46 |
4 files changed, 75 insertions, 66 deletions
diff --git a/src/components/StarterPack/QrCodeDialog.tsx b/src/components/StarterPack/QrCodeDialog.tsx index 43d8b72da..6a66e92bd 100644 --- a/src/components/StarterPack/QrCodeDialog.tsx +++ b/src/components/StarterPack/QrCodeDialog.tsx @@ -4,7 +4,7 @@ import type ViewShot from 'react-native-view-shot' import {requestMediaLibraryPermissionsAsync} from 'expo-image-picker' import {createAssetAsync} from 'expo-media-library' import * as Sharing from 'expo-sharing' -import {AppBskyGraphDefs, AppBskyGraphStarterpack} from '@atproto/api' +import {type AppBskyGraphDefs, AppBskyGraphStarterpack} from '@atproto/api' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' @@ -15,7 +15,7 @@ import * as Toast from '#/view/com/util/Toast' import {atoms as a} from '#/alf' import {Button, ButtonText} from '#/components/Button' import * as Dialog from '#/components/Dialog' -import {DialogControlProps} from '#/components/Dialog' +import {type DialogControlProps} from '#/components/Dialog' import {Loader} from '#/components/Loader' import {QrCode} from '#/components/StarterPack/QrCode' import * as bsky from '#/types/bsky' @@ -55,7 +55,7 @@ export function QrCodeDialog({ if (isNative) { const res = await requestMediaLibraryPermissionsAsync() - if (!res) { + if (!res.granted) { Toast.show( _( msg`You must grant access to your photo library to save a QR code`, @@ -155,6 +155,7 @@ export function QrCodeDialog({ return ( <Dialog.Outer control={control}> + <Dialog.Handle /> <Dialog.ScrollableInner label={_(msg`Create a QR code for a starter pack`)}> <View style={[a.flex_1, a.align_center, a.gap_5xl]}> @@ -197,6 +198,7 @@ export function QrCodeDialog({ )} </React.Suspense> </View> + <Dialog.Close /> </Dialog.ScrollableInner> </Dialog.Outer> ) diff --git a/src/components/StarterPack/ShareDialog.tsx b/src/components/StarterPack/ShareDialog.tsx index 44d5eb816..c159b42dd 100644 --- a/src/components/StarterPack/ShareDialog.tsx +++ b/src/components/StarterPack/ShareDialog.tsx @@ -1,18 +1,15 @@ import {View} from 'react-native' import {Image} from 'expo-image' -import {requestMediaLibraryPermissionsAsync} from 'expo-image-picker' import {type AppBskyGraphDefs} from '@atproto/api' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' -import {saveImageToMediaLibrary} from '#/lib/media/manip' +import {useSaveImageToMediaLibrary} from '#/lib/media/save-image' import {shareUrl} from '#/lib/sharing' import {logEvent} from '#/lib/statsig/statsig' import {getStarterPackOgCard} from '#/lib/strings/starter-pack' -import {logger} from '#/logger' import {isNative, isWeb} from '#/platform/detection' -import * as Toast from '#/view/com/util/Toast' import {atoms as a, useTheme} from '#/alf' import {Button, ButtonText} from '#/components/Button' import {type DialogControlProps} from '#/components/Dialog' @@ -60,26 +57,10 @@ function ShareDialogInner({ control.close() } - const onSave = async () => { - const res = await requestMediaLibraryPermissionsAsync() - - if (!res) { - Toast.show( - _(msg`You must grant access to your photo library to save the image.`), - 'xmark', - ) - return - } + const saveImageToAlbum = useSaveImageToMediaLibrary() - try { - await saveImageToMediaLibrary({uri: imageUrl}) - Toast.show(_(msg`Image saved`)) - control.close() - } catch (e: unknown) { - Toast.show(_(msg`An error occurred while saving the QR code!`), 'xmark') - logger.error('Failed to save QR code', {error: e}) - return - } + const onSave = async () => { + await saveImageToAlbum(imageUrl) } return ( @@ -161,6 +142,7 @@ function ShareDialogInner({ </View> </View> )} + <Dialog.Close /> </Dialog.ScrollableInner> </> ) diff --git a/src/lib/media/save-image.ts b/src/lib/media/save-image.ts new file mode 100644 index 000000000..47955b15c --- /dev/null +++ b/src/lib/media/save-image.ts @@ -0,0 +1,59 @@ +import {useCallback} from 'react' +import * as MediaLibrary from 'expo-media-library' +import {t} from '@lingui/macro' + +import {isNative} from '#/platform/detection' +import * as Toast from '#/view/com/util/Toast' +import {saveImageToMediaLibrary} from './manip' + +/** + * Same as `saveImageToMediaLibrary`, but also handles permissions and toasts + */ +export function useSaveImageToMediaLibrary() { + const [permissionResponse, requestPermission, getPermission] = + MediaLibrary.usePermissions({ + granularPermissions: ['photo'], + }) + return useCallback( + async (uri: string) => { + if (!isNative) { + throw new Error('useSaveImageToMediaLibrary is native only') + } + + async function save() { + try { + await saveImageToMediaLibrary({uri}) + Toast.show(t`Image saved`) + } catch (e: any) { + Toast.show(t`Failed to save image: ${String(e)}`, 'xmark') + } + } + + const permission = permissionResponse ?? (await getPermission()) + + if (permission.granted) { + await save() + } else { + if (permission.canAskAgain) { + // request again once + const askAgain = await requestPermission() + if (askAgain.granted) { + await save() + } else { + // since we've been explicitly denied, show a toast. + Toast.show( + t`Images cannot be saved unless permission is granted to access your photo library.`, + 'xmark', + ) + } + } else { + Toast.show( + t`Permission to access your photo library was denied. Please enable it in your system settings.`, + 'xmark', + ) + } + } + }, + [permissionResponse, requestPermission, getPermission], + ) +} diff --git a/src/view/com/lightbox/Lightbox.tsx b/src/view/com/lightbox/Lightbox.tsx index 677256191..003d01a94 100644 --- a/src/view/com/lightbox/Lightbox.tsx +++ b/src/view/com/lightbox/Lightbox.tsx @@ -1,59 +1,25 @@ -import React from 'react' -import * as MediaLibrary from 'expo-media-library' -import {msg} from '@lingui/macro' -import {useLingui} from '@lingui/react' +import {useCallback} from 'react' -import {saveImageToMediaLibrary, shareImageModal} from '#/lib/media/manip' +import {shareImageModal} from '#/lib/media/manip' +import {useSaveImageToMediaLibrary} from '#/lib/media/save-image' import {useLightbox, useLightboxControls} from '#/state/lightbox' -import * as Toast from '../util/Toast' import ImageView from './ImageViewing' export function Lightbox() { const {activeLightbox} = useLightbox() const {closeLightbox} = useLightboxControls() - const onClose = React.useCallback(() => { + const onClose = useCallback(() => { closeLightbox() }, [closeLightbox]) - const {_} = useLingui() - const [permissionResponse, requestPermission] = MediaLibrary.usePermissions({ - granularPermissions: ['photo'], - }) - const saveImageToAlbumWithToasts = React.useCallback( - async (uri: string) => { - if (!permissionResponse || permissionResponse.granted === false) { - Toast.show( - _(msg`Permission to access camera roll is required.`), - 'info', - ) - if (permissionResponse?.canAskAgain) { - requestPermission() - } else { - Toast.show( - _( - msg`Permission to access camera roll was denied. Please enable it in your system settings.`, - ), - 'xmark', - ) - } - return - } - try { - await saveImageToMediaLibrary({uri}) - Toast.show(_(msg`Image saved`)) - } catch (e: any) { - Toast.show(_(msg`Failed to save image: ${String(e)}`), 'xmark') - } - }, - [permissionResponse, requestPermission, _], - ) + const saveImageToAlbum = useSaveImageToMediaLibrary() return ( <ImageView lightbox={activeLightbox} onRequestClose={onClose} - onPressSave={saveImageToAlbumWithToasts} + onPressSave={saveImageToAlbum} onPressShare={uri => shareImageModal({uri})} /> ) |