···44import {requestMediaLibraryPermissionsAsync} from 'expo-image-picker'
55import {createAssetAsync} from 'expo-media-library'
66import * as Sharing from 'expo-sharing'
77-import {AppBskyGraphDefs, AppBskyGraphStarterpack} from '@atproto/api'
77+import {type AppBskyGraphDefs, AppBskyGraphStarterpack} from '@atproto/api'
88import {msg, Trans} from '@lingui/macro'
99import {useLingui} from '@lingui/react'
1010···1515import {atoms as a} from '#/alf'
1616import {Button, ButtonText} from '#/components/Button'
1717import * as Dialog from '#/components/Dialog'
1818-import {DialogControlProps} from '#/components/Dialog'
1818+import {type DialogControlProps} from '#/components/Dialog'
1919import {Loader} from '#/components/Loader'
2020import {QrCode} from '#/components/StarterPack/QrCode'
2121import * as bsky from '#/types/bsky'
···5555 if (isNative) {
5656 const res = await requestMediaLibraryPermissionsAsync()
57575858- if (!res) {
5858+ if (!res.granted) {
5959 Toast.show(
6060 _(
6161 msg`You must grant access to your photo library to save a QR code`,
···155155156156 return (
157157 <Dialog.Outer control={control}>
158158+ <Dialog.Handle />
158159 <Dialog.ScrollableInner
159160 label={_(msg`Create a QR code for a starter pack`)}>
160161 <View style={[a.flex_1, a.align_center, a.gap_5xl]}>
···197198 )}
198199 </React.Suspense>
199200 </View>
201201+ <Dialog.Close />
200202 </Dialog.ScrollableInner>
201203 </Dialog.Outer>
202204 )
+5-23
src/components/StarterPack/ShareDialog.tsx
···11import {View} from 'react-native'
22import {Image} from 'expo-image'
33-import {requestMediaLibraryPermissionsAsync} from 'expo-image-picker'
43import {type AppBskyGraphDefs} from '@atproto/api'
54import {msg, Trans} from '@lingui/macro'
65import {useLingui} from '@lingui/react'
7687import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
99-import {saveImageToMediaLibrary} from '#/lib/media/manip'
88+import {useSaveImageToMediaLibrary} from '#/lib/media/save-image'
109import {shareUrl} from '#/lib/sharing'
1110import {logEvent} from '#/lib/statsig/statsig'
1211import {getStarterPackOgCard} from '#/lib/strings/starter-pack'
1313-import {logger} from '#/logger'
1412import {isNative, isWeb} from '#/platform/detection'
1515-import * as Toast from '#/view/com/util/Toast'
1613import {atoms as a, useTheme} from '#/alf'
1714import {Button, ButtonText} from '#/components/Button'
1815import {type DialogControlProps} from '#/components/Dialog'
···6057 control.close()
6158 }
62596363- const onSave = async () => {
6464- const res = await requestMediaLibraryPermissionsAsync()
6565-6666- if (!res) {
6767- Toast.show(
6868- _(msg`You must grant access to your photo library to save the image.`),
6969- 'xmark',
7070- )
7171- return
7272- }
6060+ const saveImageToAlbum = useSaveImageToMediaLibrary()
73617474- try {
7575- await saveImageToMediaLibrary({uri: imageUrl})
7676- Toast.show(_(msg`Image saved`))
7777- control.close()
7878- } catch (e: unknown) {
7979- Toast.show(_(msg`An error occurred while saving the QR code!`), 'xmark')
8080- logger.error('Failed to save QR code', {error: e})
8181- return
8282- }
6262+ const onSave = async () => {
6363+ await saveImageToAlbum(imageUrl)
8364 }
84658566 return (
···161142 </View>
162143 </View>
163144 )}
145145+ <Dialog.Close />
164146 </Dialog.ScrollableInner>
165147 </>
166148 )
+59
src/lib/media/save-image.ts
···11+import {useCallback} from 'react'
22+import * as MediaLibrary from 'expo-media-library'
33+import {t} from '@lingui/macro'
44+55+import {isNative} from '#/platform/detection'
66+import * as Toast from '#/view/com/util/Toast'
77+import {saveImageToMediaLibrary} from './manip'
88+99+/**
1010+ * Same as `saveImageToMediaLibrary`, but also handles permissions and toasts
1111+ */
1212+export function useSaveImageToMediaLibrary() {
1313+ const [permissionResponse, requestPermission, getPermission] =
1414+ MediaLibrary.usePermissions({
1515+ granularPermissions: ['photo'],
1616+ })
1717+ return useCallback(
1818+ async (uri: string) => {
1919+ if (!isNative) {
2020+ throw new Error('useSaveImageToMediaLibrary is native only')
2121+ }
2222+2323+ async function save() {
2424+ try {
2525+ await saveImageToMediaLibrary({uri})
2626+ Toast.show(t`Image saved`)
2727+ } catch (e: any) {
2828+ Toast.show(t`Failed to save image: ${String(e)}`, 'xmark')
2929+ }
3030+ }
3131+3232+ const permission = permissionResponse ?? (await getPermission())
3333+3434+ if (permission.granted) {
3535+ await save()
3636+ } else {
3737+ if (permission.canAskAgain) {
3838+ // request again once
3939+ const askAgain = await requestPermission()
4040+ if (askAgain.granted) {
4141+ await save()
4242+ } else {
4343+ // since we've been explicitly denied, show a toast.
4444+ Toast.show(
4545+ t`Images cannot be saved unless permission is granted to access your photo library.`,
4646+ 'xmark',
4747+ )
4848+ }
4949+ } else {
5050+ Toast.show(
5151+ t`Permission to access your photo library was denied. Please enable it in your system settings.`,
5252+ 'xmark',
5353+ )
5454+ }
5555+ }
5656+ },
5757+ [permissionResponse, requestPermission, getPermission],
5858+ )
5959+}
+6-40
src/view/com/lightbox/Lightbox.tsx
···11-import React from 'react'
22-import * as MediaLibrary from 'expo-media-library'
33-import {msg} from '@lingui/macro'
44-import {useLingui} from '@lingui/react'
11+import {useCallback} from 'react'
5266-import {saveImageToMediaLibrary, shareImageModal} from '#/lib/media/manip'
33+import {shareImageModal} from '#/lib/media/manip'
44+import {useSaveImageToMediaLibrary} from '#/lib/media/save-image'
75import {useLightbox, useLightboxControls} from '#/state/lightbox'
88-import * as Toast from '../util/Toast'
96import ImageView from './ImageViewing'
107118export function Lightbox() {
129 const {activeLightbox} = useLightbox()
1310 const {closeLightbox} = useLightboxControls()
14111515- const onClose = React.useCallback(() => {
1212+ const onClose = useCallback(() => {
1613 closeLightbox()
1714 }, [closeLightbox])
18151919- const {_} = useLingui()
2020- const [permissionResponse, requestPermission] = MediaLibrary.usePermissions({
2121- granularPermissions: ['photo'],
2222- })
2323- const saveImageToAlbumWithToasts = React.useCallback(
2424- async (uri: string) => {
2525- if (!permissionResponse || permissionResponse.granted === false) {
2626- Toast.show(
2727- _(msg`Permission to access camera roll is required.`),
2828- 'info',
2929- )
3030- if (permissionResponse?.canAskAgain) {
3131- requestPermission()
3232- } else {
3333- Toast.show(
3434- _(
3535- msg`Permission to access camera roll was denied. Please enable it in your system settings.`,
3636- ),
3737- 'xmark',
3838- )
3939- }
4040- return
4141- }
4242- try {
4343- await saveImageToMediaLibrary({uri})
4444- Toast.show(_(msg`Image saved`))
4545- } catch (e: any) {
4646- Toast.show(_(msg`Failed to save image: ${String(e)}`), 'xmark')
4747- }
4848- },
4949- [permissionResponse, requestPermission, _],
5050- )
1616+ const saveImageToAlbum = useSaveImageToMediaLibrary()
51175218 return (
5319 <ImageView
5420 lightbox={activeLightbox}
5521 onRequestClose={onClose}
5656- onPressSave={saveImageToAlbumWithToasts}
2222+ onPressSave={saveImageToAlbum}
5723 onPressShare={uri => shareImageModal({uri})}
5824 />
5925 )