Bluesky app fork with some witchin' additions 💫 witchsky.app
bluesky fork client

Speculative fix to Android camera roll issue (#8397)

authored by samuel.fm and committed by

GitHub a0ea6343 c16cd36b

+75 -66
+5 -3
src/components/StarterPack/QrCodeDialog.tsx
··· 4 4 import {requestMediaLibraryPermissionsAsync} from 'expo-image-picker' 5 5 import {createAssetAsync} from 'expo-media-library' 6 6 import * as Sharing from 'expo-sharing' 7 - import {AppBskyGraphDefs, AppBskyGraphStarterpack} from '@atproto/api' 7 + import {type AppBskyGraphDefs, AppBskyGraphStarterpack} from '@atproto/api' 8 8 import {msg, Trans} from '@lingui/macro' 9 9 import {useLingui} from '@lingui/react' 10 10 ··· 15 15 import {atoms as a} from '#/alf' 16 16 import {Button, ButtonText} from '#/components/Button' 17 17 import * as Dialog from '#/components/Dialog' 18 - import {DialogControlProps} from '#/components/Dialog' 18 + import {type DialogControlProps} from '#/components/Dialog' 19 19 import {Loader} from '#/components/Loader' 20 20 import {QrCode} from '#/components/StarterPack/QrCode' 21 21 import * as bsky from '#/types/bsky' ··· 55 55 if (isNative) { 56 56 const res = await requestMediaLibraryPermissionsAsync() 57 57 58 - if (!res) { 58 + if (!res.granted) { 59 59 Toast.show( 60 60 _( 61 61 msg`You must grant access to your photo library to save a QR code`, ··· 155 155 156 156 return ( 157 157 <Dialog.Outer control={control}> 158 + <Dialog.Handle /> 158 159 <Dialog.ScrollableInner 159 160 label={_(msg`Create a QR code for a starter pack`)}> 160 161 <View style={[a.flex_1, a.align_center, a.gap_5xl]}> ··· 197 198 )} 198 199 </React.Suspense> 199 200 </View> 201 + <Dialog.Close /> 200 202 </Dialog.ScrollableInner> 201 203 </Dialog.Outer> 202 204 )
+5 -23
src/components/StarterPack/ShareDialog.tsx
··· 1 1 import {View} from 'react-native' 2 2 import {Image} from 'expo-image' 3 - import {requestMediaLibraryPermissionsAsync} from 'expo-image-picker' 4 3 import {type AppBskyGraphDefs} from '@atproto/api' 5 4 import {msg, Trans} from '@lingui/macro' 6 5 import {useLingui} from '@lingui/react' 7 6 8 7 import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' 9 - import {saveImageToMediaLibrary} from '#/lib/media/manip' 8 + import {useSaveImageToMediaLibrary} from '#/lib/media/save-image' 10 9 import {shareUrl} from '#/lib/sharing' 11 10 import {logEvent} from '#/lib/statsig/statsig' 12 11 import {getStarterPackOgCard} from '#/lib/strings/starter-pack' 13 - import {logger} from '#/logger' 14 12 import {isNative, isWeb} from '#/platform/detection' 15 - import * as Toast from '#/view/com/util/Toast' 16 13 import {atoms as a, useTheme} from '#/alf' 17 14 import {Button, ButtonText} from '#/components/Button' 18 15 import {type DialogControlProps} from '#/components/Dialog' ··· 60 57 control.close() 61 58 } 62 59 63 - const onSave = async () => { 64 - const res = await requestMediaLibraryPermissionsAsync() 65 - 66 - if (!res) { 67 - Toast.show( 68 - _(msg`You must grant access to your photo library to save the image.`), 69 - 'xmark', 70 - ) 71 - return 72 - } 60 + const saveImageToAlbum = useSaveImageToMediaLibrary() 73 61 74 - try { 75 - await saveImageToMediaLibrary({uri: imageUrl}) 76 - Toast.show(_(msg`Image saved`)) 77 - control.close() 78 - } catch (e: unknown) { 79 - Toast.show(_(msg`An error occurred while saving the QR code!`), 'xmark') 80 - logger.error('Failed to save QR code', {error: e}) 81 - return 82 - } 62 + const onSave = async () => { 63 + await saveImageToAlbum(imageUrl) 83 64 } 84 65 85 66 return ( ··· 161 142 </View> 162 143 </View> 163 144 )} 145 + <Dialog.Close /> 164 146 </Dialog.ScrollableInner> 165 147 </> 166 148 )
+59
src/lib/media/save-image.ts
··· 1 + import {useCallback} from 'react' 2 + import * as MediaLibrary from 'expo-media-library' 3 + import {t} from '@lingui/macro' 4 + 5 + import {isNative} from '#/platform/detection' 6 + import * as Toast from '#/view/com/util/Toast' 7 + import {saveImageToMediaLibrary} from './manip' 8 + 9 + /** 10 + * Same as `saveImageToMediaLibrary`, but also handles permissions and toasts 11 + */ 12 + export function useSaveImageToMediaLibrary() { 13 + const [permissionResponse, requestPermission, getPermission] = 14 + MediaLibrary.usePermissions({ 15 + granularPermissions: ['photo'], 16 + }) 17 + return useCallback( 18 + async (uri: string) => { 19 + if (!isNative) { 20 + throw new Error('useSaveImageToMediaLibrary is native only') 21 + } 22 + 23 + async function save() { 24 + try { 25 + await saveImageToMediaLibrary({uri}) 26 + Toast.show(t`Image saved`) 27 + } catch (e: any) { 28 + Toast.show(t`Failed to save image: ${String(e)}`, 'xmark') 29 + } 30 + } 31 + 32 + const permission = permissionResponse ?? (await getPermission()) 33 + 34 + if (permission.granted) { 35 + await save() 36 + } else { 37 + if (permission.canAskAgain) { 38 + // request again once 39 + const askAgain = await requestPermission() 40 + if (askAgain.granted) { 41 + await save() 42 + } else { 43 + // since we've been explicitly denied, show a toast. 44 + Toast.show( 45 + t`Images cannot be saved unless permission is granted to access your photo library.`, 46 + 'xmark', 47 + ) 48 + } 49 + } else { 50 + Toast.show( 51 + t`Permission to access your photo library was denied. Please enable it in your system settings.`, 52 + 'xmark', 53 + ) 54 + } 55 + } 56 + }, 57 + [permissionResponse, requestPermission, getPermission], 58 + ) 59 + }
+6 -40
src/view/com/lightbox/Lightbox.tsx
··· 1 - import React from 'react' 2 - import * as MediaLibrary from 'expo-media-library' 3 - import {msg} from '@lingui/macro' 4 - import {useLingui} from '@lingui/react' 1 + import {useCallback} from 'react' 5 2 6 - import {saveImageToMediaLibrary, shareImageModal} from '#/lib/media/manip' 3 + import {shareImageModal} from '#/lib/media/manip' 4 + import {useSaveImageToMediaLibrary} from '#/lib/media/save-image' 7 5 import {useLightbox, useLightboxControls} from '#/state/lightbox' 8 - import * as Toast from '../util/Toast' 9 6 import ImageView from './ImageViewing' 10 7 11 8 export function Lightbox() { 12 9 const {activeLightbox} = useLightbox() 13 10 const {closeLightbox} = useLightboxControls() 14 11 15 - const onClose = React.useCallback(() => { 12 + const onClose = useCallback(() => { 16 13 closeLightbox() 17 14 }, [closeLightbox]) 18 15 19 - const {_} = useLingui() 20 - const [permissionResponse, requestPermission] = MediaLibrary.usePermissions({ 21 - granularPermissions: ['photo'], 22 - }) 23 - const saveImageToAlbumWithToasts = React.useCallback( 24 - async (uri: string) => { 25 - if (!permissionResponse || permissionResponse.granted === false) { 26 - Toast.show( 27 - _(msg`Permission to access camera roll is required.`), 28 - 'info', 29 - ) 30 - if (permissionResponse?.canAskAgain) { 31 - requestPermission() 32 - } else { 33 - Toast.show( 34 - _( 35 - msg`Permission to access camera roll was denied. Please enable it in your system settings.`, 36 - ), 37 - 'xmark', 38 - ) 39 - } 40 - return 41 - } 42 - try { 43 - await saveImageToMediaLibrary({uri}) 44 - Toast.show(_(msg`Image saved`)) 45 - } catch (e: any) { 46 - Toast.show(_(msg`Failed to save image: ${String(e)}`), 'xmark') 47 - } 48 - }, 49 - [permissionResponse, requestPermission, _], 50 - ) 16 + const saveImageToAlbum = useSaveImageToMediaLibrary() 51 17 52 18 return ( 53 19 <ImageView 54 20 lightbox={activeLightbox} 55 21 onRequestClose={onClose} 56 - onPressSave={saveImageToAlbumWithToasts} 22 + onPressSave={saveImageToAlbum} 57 23 onPressShare={uri => shareImageModal({uri})} 58 24 /> 59 25 )