Fix photo & camera permission management (#140)
* Check photo & camera perms and alert the user if not available (close #64) - Adds perms checks with a prompt to update settings if needed - Moves initial access of photos in the composer so that the initial prompt occurs at an intuitive time. * Add react-native-permissions test mock * Fix issue causing multiple access requests * Use longer var names * Update podfile.lock * Lint fix * Move photo perm request in composer to the gallery btn instead of when the carousel is opened
This commit is contained in:
@@ -44,6 +44,11 @@ target 'app' do
|
|||||||
:app_path => "#{Pod::Config.instance.installation_root}/.."
|
:app_path => "#{Pod::Config.instance.installation_root}/.."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# react-native-permissions settings
|
||||||
|
permissions_path = '../node_modules/react-native-permissions/ios'
|
||||||
|
pod 'Permission-Camera', :path => "#{permissions_path}/Camera"
|
||||||
|
pod 'Permission-PhotoLibrary', :path => "#{permissions_path}/PhotoLibrary"
|
||||||
|
|
||||||
target 'appTests' do
|
target 'appTests' do
|
||||||
inherit! :complete
|
inherit! :complete
|
||||||
# Pods for testing
|
# Pods for testing
|
||||||
|
|||||||
+19
-1
@@ -26,6 +26,10 @@ PODS:
|
|||||||
- libwebp/mux (1.2.4):
|
- libwebp/mux (1.2.4):
|
||||||
- libwebp/demux
|
- libwebp/demux
|
||||||
- libwebp/webp (1.2.4)
|
- libwebp/webp (1.2.4)
|
||||||
|
- Permission-Camera (3.6.1):
|
||||||
|
- RNPermissions
|
||||||
|
- Permission-PhotoLibrary (3.6.1):
|
||||||
|
- RNPermissions
|
||||||
- RCT-Folly (2021.07.22.00):
|
- RCT-Folly (2021.07.22.00):
|
||||||
- boost
|
- boost
|
||||||
- DoubleConversion
|
- DoubleConversion
|
||||||
@@ -379,6 +383,8 @@ PODS:
|
|||||||
- RNNotifee/NotifeeCore (= 7.4.0)
|
- RNNotifee/NotifeeCore (= 7.4.0)
|
||||||
- RNNotifee/NotifeeCore (7.4.0):
|
- RNNotifee/NotifeeCore (7.4.0):
|
||||||
- React-Core
|
- React-Core
|
||||||
|
- RNPermissions (3.6.1):
|
||||||
|
- React-Core
|
||||||
- RNReactNativeHapticFeedback (1.14.0):
|
- RNReactNativeHapticFeedback (1.14.0):
|
||||||
- React-Core
|
- React-Core
|
||||||
- RNReanimated (2.13.0):
|
- RNReanimated (2.13.0):
|
||||||
@@ -437,6 +443,8 @@ DEPENDENCIES:
|
|||||||
- glog (from `../node_modules/react-native/third-party-podspecs/glog.podspec`)
|
- glog (from `../node_modules/react-native/third-party-podspecs/glog.podspec`)
|
||||||
- hermes-engine (from `../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec`)
|
- hermes-engine (from `../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec`)
|
||||||
- libevent (~> 2.1.12)
|
- libevent (~> 2.1.12)
|
||||||
|
- Permission-Camera (from `../node_modules/react-native-permissions/ios/Camera`)
|
||||||
|
- Permission-PhotoLibrary (from `../node_modules/react-native-permissions/ios/PhotoLibrary`)
|
||||||
- RCT-Folly (from `../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`)
|
- RCT-Folly (from `../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`)
|
||||||
- RCTRequired (from `../node_modules/react-native/Libraries/RCTRequired`)
|
- RCTRequired (from `../node_modules/react-native/Libraries/RCTRequired`)
|
||||||
- RCTTypeSafety (from `../node_modules/react-native/Libraries/TypeSafety`)
|
- RCTTypeSafety (from `../node_modules/react-native/Libraries/TypeSafety`)
|
||||||
@@ -483,6 +491,7 @@ DEPENDENCIES:
|
|||||||
- RNImageCropPicker (from `../node_modules/react-native-image-crop-picker`)
|
- RNImageCropPicker (from `../node_modules/react-native-image-crop-picker`)
|
||||||
- RNInAppBrowser (from `../node_modules/react-native-inappbrowser-reborn`)
|
- RNInAppBrowser (from `../node_modules/react-native-inappbrowser-reborn`)
|
||||||
- "RNNotifee (from `../node_modules/@notifee/react-native`)"
|
- "RNNotifee (from `../node_modules/@notifee/react-native`)"
|
||||||
|
- RNPermissions (from `../node_modules/react-native-permissions`)
|
||||||
- RNReactNativeHapticFeedback (from `../node_modules/react-native-haptic-feedback`)
|
- RNReactNativeHapticFeedback (from `../node_modules/react-native-haptic-feedback`)
|
||||||
- RNReanimated (from `../node_modules/react-native-reanimated`)
|
- RNReanimated (from `../node_modules/react-native-reanimated`)
|
||||||
- RNScreens (from `../node_modules/react-native-screens`)
|
- RNScreens (from `../node_modules/react-native-screens`)
|
||||||
@@ -516,6 +525,10 @@ EXTERNAL SOURCES:
|
|||||||
:podspec: "../node_modules/react-native/third-party-podspecs/glog.podspec"
|
:podspec: "../node_modules/react-native/third-party-podspecs/glog.podspec"
|
||||||
hermes-engine:
|
hermes-engine:
|
||||||
:podspec: "../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec"
|
:podspec: "../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec"
|
||||||
|
Permission-Camera:
|
||||||
|
:path: "../node_modules/react-native-permissions/ios/Camera"
|
||||||
|
Permission-PhotoLibrary:
|
||||||
|
:path: "../node_modules/react-native-permissions/ios/PhotoLibrary"
|
||||||
RCT-Folly:
|
RCT-Folly:
|
||||||
:podspec: "../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec"
|
:podspec: "../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec"
|
||||||
RCTRequired:
|
RCTRequired:
|
||||||
@@ -606,6 +619,8 @@ EXTERNAL SOURCES:
|
|||||||
:path: "../node_modules/react-native-inappbrowser-reborn"
|
:path: "../node_modules/react-native-inappbrowser-reborn"
|
||||||
RNNotifee:
|
RNNotifee:
|
||||||
:path: "../node_modules/@notifee/react-native"
|
:path: "../node_modules/@notifee/react-native"
|
||||||
|
RNPermissions:
|
||||||
|
:path: "../node_modules/react-native-permissions"
|
||||||
RNReactNativeHapticFeedback:
|
RNReactNativeHapticFeedback:
|
||||||
:path: "../node_modules/react-native-haptic-feedback"
|
:path: "../node_modules/react-native-haptic-feedback"
|
||||||
RNReanimated:
|
RNReanimated:
|
||||||
@@ -632,6 +647,8 @@ SPEC CHECKSUMS:
|
|||||||
hermes-engine: f6e715aa6c8bd38de6c13bc85e07b0a337edaa89
|
hermes-engine: f6e715aa6c8bd38de6c13bc85e07b0a337edaa89
|
||||||
libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913
|
libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913
|
||||||
libwebp: f62cb61d0a484ba548448a4bd52aabf150ff6eef
|
libwebp: f62cb61d0a484ba548448a4bd52aabf150ff6eef
|
||||||
|
Permission-Camera: bf6791b17c7f614b6826019fcfdcc286d3a107f6
|
||||||
|
Permission-PhotoLibrary: 5b34ca67279f7201ae109cef36f9806a6596002d
|
||||||
RCT-Folly: 424b8c9a7a0b9ab2886ffe9c3b041ef628fd4fb1
|
RCT-Folly: 424b8c9a7a0b9ab2886ffe9c3b041ef628fd4fb1
|
||||||
RCTRequired: fd4d923b964658aa0c4091a32c8b2004c6d9e3a6
|
RCTRequired: fd4d923b964658aa0c4091a32c8b2004c6d9e3a6
|
||||||
RCTTypeSafety: c276d85975bde3d8448907235c70bf0da257adfd
|
RCTTypeSafety: c276d85975bde3d8448907235c70bf0da257adfd
|
||||||
@@ -677,6 +694,7 @@ SPEC CHECKSUMS:
|
|||||||
RNImageCropPicker: 648356d68fbf9911a1016b3e3723885d28373eda
|
RNImageCropPicker: 648356d68fbf9911a1016b3e3723885d28373eda
|
||||||
RNInAppBrowser: e36d6935517101ccba0e875bac8ad7b0cb655364
|
RNInAppBrowser: e36d6935517101ccba0e875bac8ad7b0cb655364
|
||||||
RNNotifee: da8dcf09f079ea22f46e239d7c406e10d4525a5f
|
RNNotifee: da8dcf09f079ea22f46e239d7c406e10d4525a5f
|
||||||
|
RNPermissions: dcdb7b99796bbeda6975a6e79ad519c41b251b1c
|
||||||
RNReactNativeHapticFeedback: 1e3efeca9628ff9876ee7cdd9edec1b336913f8c
|
RNReactNativeHapticFeedback: 1e3efeca9628ff9876ee7cdd9edec1b336913f8c
|
||||||
RNReanimated: d8d9d3d3801bda5e35e85cdffc871577d044dc2e
|
RNReanimated: d8d9d3d3801bda5e35e85cdffc871577d044dc2e
|
||||||
RNScreens: 34cc502acf1b916c582c60003dc3089fa01dc66d
|
RNScreens: 34cc502acf1b916c582c60003dc3089fa01dc66d
|
||||||
@@ -689,6 +707,6 @@ SPEC CHECKSUMS:
|
|||||||
TOCropViewController: edfd4f25713d56905ad1e0b9f5be3fbe0f59c863
|
TOCropViewController: edfd4f25713d56905ad1e0b9f5be3fbe0f59c863
|
||||||
Yoga: 921eb014669cf9c718ada68b08d362517d564e0c
|
Yoga: 921eb014669cf9c718ada68b08d362517d564e0c
|
||||||
|
|
||||||
PODFILE CHECKSUM: 32d3f4ec61d2c786d46fd58000f53917922dbb8a
|
PODFILE CHECKSUM: 95c7fde1130d862b561348cca2b3fb7f9bd84bfb
|
||||||
|
|
||||||
COCOAPODS: 1.11.3
|
COCOAPODS: 1.11.3
|
||||||
|
|||||||
@@ -55,3 +55,7 @@ jest.mock('@segment/analytics-react-native', () => ({
|
|||||||
flush: jest.fn(),
|
flush: jest.fn(),
|
||||||
}),
|
}),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
jest.mock('react-native-permissions', () =>
|
||||||
|
require('react-native-permissions/mock'),
|
||||||
|
)
|
||||||
|
|||||||
@@ -56,6 +56,7 @@
|
|||||||
"react-native-inappbrowser-reborn": "^3.6.3",
|
"react-native-inappbrowser-reborn": "^3.6.3",
|
||||||
"react-native-linear-gradient": "^2.6.2",
|
"react-native-linear-gradient": "^2.6.2",
|
||||||
"react-native-pager-view": "^6.0.2",
|
"react-native-pager-view": "^6.0.2",
|
||||||
|
"react-native-permissions": "^3.6.1",
|
||||||
"react-native-progress": "^5.0.0",
|
"react-native-progress": "^5.0.0",
|
||||||
"react-native-reanimated": "^2.9.1",
|
"react-native-reanimated": "^2.9.1",
|
||||||
"react-native-root-siblings": "^4.1.1",
|
"react-native-root-siblings": "^4.1.1",
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import {Alert} from 'react-native'
|
||||||
|
import {
|
||||||
|
check,
|
||||||
|
openSettings,
|
||||||
|
Permission,
|
||||||
|
PermissionStatus,
|
||||||
|
PERMISSIONS,
|
||||||
|
RESULTS,
|
||||||
|
} from 'react-native-permissions'
|
||||||
|
|
||||||
|
export const PHOTO_LIBRARY = PERMISSIONS.IOS.PHOTO_LIBRARY
|
||||||
|
export const CAMERA = PERMISSIONS.IOS.CAMERA
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns `true` if the user has granted permission or hasn't made
|
||||||
|
* a decision yet. Returns `false` if unavailable or not granted.
|
||||||
|
*/
|
||||||
|
export async function hasAccess(perm: Permission): Promise<boolean> {
|
||||||
|
const status = await check(perm)
|
||||||
|
return isntANo(status)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function requestAccessIfNeeded(
|
||||||
|
perm: Permission,
|
||||||
|
): Promise<boolean> {
|
||||||
|
if (await hasAccess(perm)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
let permDescription
|
||||||
|
if (perm === PHOTO_LIBRARY) {
|
||||||
|
permDescription = 'photo library'
|
||||||
|
} else if (perm === CAMERA) {
|
||||||
|
permDescription = 'camera'
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
Alert.alert(
|
||||||
|
'Permission needed',
|
||||||
|
`Bluesky does not have permission to access your ${permDescription}.`,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
text: 'Cancel',
|
||||||
|
style: 'cancel',
|
||||||
|
},
|
||||||
|
{text: 'Open Settings', onPress: () => openSettings()},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function requestPhotoAccessIfNeeded() {
|
||||||
|
return requestAccessIfNeeded(PHOTO_LIBRARY)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function requestCameraAccessIfNeeded() {
|
||||||
|
return requestAccessIfNeeded(CAMERA)
|
||||||
|
}
|
||||||
|
|
||||||
|
function isntANo(status: PermissionStatus): boolean {
|
||||||
|
return status !== RESULTS.UNAVAILABLE && status !== RESULTS.BLOCKED
|
||||||
|
}
|
||||||
@@ -20,7 +20,7 @@ export class UserLocalPhotosModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async _getPhotos() {
|
private async _getPhotos() {
|
||||||
CameraRoll.getPhotos({first: 20}).then(r => {
|
return CameraRoll.getPhotos({first: 20}).then(r => {
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.photos = r.edges
|
this.photos = r.edges
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -43,7 +43,6 @@ import {
|
|||||||
} from '../../../lib/strings'
|
} from '../../../lib/strings'
|
||||||
import {getLinkMeta} from '../../../lib/link-meta'
|
import {getLinkMeta} from '../../../lib/link-meta'
|
||||||
import {downloadAndResize} from '../../../lib/images'
|
import {downloadAndResize} from '../../../lib/images'
|
||||||
import {UserLocalPhotosModel} from '../../../state/models/user-local-photos'
|
|
||||||
import {getMentionAt, insertMentionAt} from '../../../lib/strings/mention-manip'
|
import {getMentionAt, insertMentionAt} from '../../../lib/strings/mention-manip'
|
||||||
import {PhotoCarouselPicker, cropPhoto} from './PhotoCarouselPicker'
|
import {PhotoCarouselPicker, cropPhoto} from './PhotoCarouselPicker'
|
||||||
import {SelectedPhoto} from './SelectedPhoto'
|
import {SelectedPhoto} from './SelectedPhoto'
|
||||||
@@ -94,10 +93,6 @@ export const ComposePost = observer(function ComposePost({
|
|||||||
() => new UserAutocompleteViewModel(store),
|
() => new UserAutocompleteViewModel(store),
|
||||||
[store],
|
[store],
|
||||||
)
|
)
|
||||||
const localPhotos = React.useMemo<UserLocalPhotosModel>(
|
|
||||||
() => new UserLocalPhotosModel(store),
|
|
||||||
[store],
|
|
||||||
)
|
|
||||||
|
|
||||||
// HACK
|
// HACK
|
||||||
// there's a bug with @mattermost/react-native-paste-input where if the input
|
// there's a bug with @mattermost/react-native-paste-input where if the input
|
||||||
@@ -112,8 +107,7 @@ export const ComposePost = observer(function ComposePost({
|
|||||||
// initial setup
|
// initial setup
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
autocompleteView.setup()
|
autocompleteView.setup()
|
||||||
localPhotos.setup()
|
}, [autocompleteView])
|
||||||
}, [autocompleteView, localPhotos])
|
|
||||||
|
|
||||||
// external link metadata-fetch flow
|
// external link metadata-fetch flow
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -436,13 +430,10 @@ export const ComposePost = observer(function ComposePost({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
{isSelectingPhotos &&
|
{isSelectingPhotos && selectedPhotos.length < 4 ? (
|
||||||
localPhotos.photos != null &&
|
|
||||||
selectedPhotos.length < 4 ? (
|
|
||||||
<PhotoCarouselPicker
|
<PhotoCarouselPicker
|
||||||
selectedPhotos={selectedPhotos}
|
selectedPhotos={selectedPhotos}
|
||||||
onSelectPhotos={onSelectPhotos}
|
onSelectPhotos={onSelectPhotos}
|
||||||
localPhotos={localPhotos}
|
|
||||||
/>
|
/>
|
||||||
) : !extLink &&
|
) : !extLink &&
|
||||||
selectedPhotos.length === 0 &&
|
selectedPhotos.length === 0 &&
|
||||||
|
|||||||
@@ -12,6 +12,10 @@ import {
|
|||||||
UserLocalPhotosModel,
|
UserLocalPhotosModel,
|
||||||
PhotoIdentifier,
|
PhotoIdentifier,
|
||||||
} from '../../../state/models/user-local-photos'
|
} from '../../../state/models/user-local-photos'
|
||||||
|
import {
|
||||||
|
requestPhotoAccessIfNeeded,
|
||||||
|
requestCameraAccessIfNeeded,
|
||||||
|
} from '../../../lib/permissions'
|
||||||
import {compressIfNeeded, scaleDownDimensions} from '../../../lib/images'
|
import {compressIfNeeded, scaleDownDimensions} from '../../../lib/images'
|
||||||
import {usePalette} from '../../lib/hooks/usePalette'
|
import {usePalette} from '../../lib/hooks/usePalette'
|
||||||
import {useStores} from '../../../state'
|
import {useStores} from '../../../state'
|
||||||
@@ -67,16 +71,31 @@ export async function cropPhoto(
|
|||||||
export const PhotoCarouselPicker = ({
|
export const PhotoCarouselPicker = ({
|
||||||
selectedPhotos,
|
selectedPhotos,
|
||||||
onSelectPhotos,
|
onSelectPhotos,
|
||||||
localPhotos,
|
|
||||||
}: {
|
}: {
|
||||||
selectedPhotos: string[]
|
selectedPhotos: string[]
|
||||||
onSelectPhotos: (v: string[]) => void
|
onSelectPhotos: (v: string[]) => void
|
||||||
localPhotos: UserLocalPhotosModel
|
|
||||||
}) => {
|
}) => {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const store = useStores()
|
const store = useStores()
|
||||||
|
const [isSetup, setIsSetup] = React.useState<boolean>(false)
|
||||||
|
|
||||||
|
const localPhotos = React.useMemo<UserLocalPhotosModel>(
|
||||||
|
() => new UserLocalPhotosModel(store),
|
||||||
|
[store],
|
||||||
|
)
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
// initial setup
|
||||||
|
localPhotos.setup().then(() => {
|
||||||
|
setIsSetup(true)
|
||||||
|
})
|
||||||
|
}, [localPhotos])
|
||||||
|
|
||||||
const handleOpenCamera = useCallback(async () => {
|
const handleOpenCamera = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
|
if (!(await requestCameraAccessIfNeeded())) {
|
||||||
|
return
|
||||||
|
}
|
||||||
const cameraRes = await openCamera({
|
const cameraRes = await openCamera({
|
||||||
mediaType: 'photo',
|
mediaType: 'photo',
|
||||||
cropping: true,
|
cropping: true,
|
||||||
@@ -107,34 +126,36 @@ export const PhotoCarouselPicker = ({
|
|||||||
[store.log, selectedPhotos, onSelectPhotos],
|
[store.log, selectedPhotos, onSelectPhotos],
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleOpenGallery = useCallback(() => {
|
const handleOpenGallery = useCallback(async () => {
|
||||||
openPicker({
|
if (!(await requestPhotoAccessIfNeeded())) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const items = await openPicker({
|
||||||
multiple: true,
|
multiple: true,
|
||||||
maxFiles: 4 - selectedPhotos.length,
|
maxFiles: 4 - selectedPhotos.length,
|
||||||
mediaType: 'photo',
|
mediaType: 'photo',
|
||||||
}).then(async items => {
|
|
||||||
const result = []
|
|
||||||
|
|
||||||
for (const image of items) {
|
|
||||||
// choose target dimensions based on the original
|
|
||||||
// this causes the photo cropper to start with the full image "selected"
|
|
||||||
const {width, height} = scaleDownDimensions(
|
|
||||||
{width: image.width, height: image.height},
|
|
||||||
{width: MAX_WIDTH, height: MAX_HEIGHT},
|
|
||||||
)
|
|
||||||
const cropperRes = await openCropper({
|
|
||||||
mediaType: 'photo',
|
|
||||||
path: image.path,
|
|
||||||
...IMAGE_PARAMS,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
})
|
|
||||||
const finalImg = await compressIfNeeded(cropperRes, MAX_SIZE)
|
|
||||||
const permanentPath = await moveToPremanantPath(finalImg.path)
|
|
||||||
result.push(permanentPath)
|
|
||||||
}
|
|
||||||
onSelectPhotos([...selectedPhotos, ...result])
|
|
||||||
})
|
})
|
||||||
|
const result = []
|
||||||
|
|
||||||
|
for (const image of items) {
|
||||||
|
// choose target dimensions based on the original
|
||||||
|
// this causes the photo cropper to start with the full image "selected"
|
||||||
|
const {width, height} = scaleDownDimensions(
|
||||||
|
{width: image.width, height: image.height},
|
||||||
|
{width: MAX_WIDTH, height: MAX_HEIGHT},
|
||||||
|
)
|
||||||
|
const cropperRes = await openCropper({
|
||||||
|
mediaType: 'photo',
|
||||||
|
path: image.path,
|
||||||
|
...IMAGE_PARAMS,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
})
|
||||||
|
const finalImg = await compressIfNeeded(cropperRes, MAX_SIZE)
|
||||||
|
const permanentPath = await moveToPremanantPath(finalImg.path)
|
||||||
|
result.push(permanentPath)
|
||||||
|
}
|
||||||
|
onSelectPhotos([...selectedPhotos, ...result])
|
||||||
}, [selectedPhotos, onSelectPhotos])
|
}, [selectedPhotos, onSelectPhotos])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -156,15 +177,16 @@ export const PhotoCarouselPicker = ({
|
|||||||
onPress={handleOpenGallery}>
|
onPress={handleOpenGallery}>
|
||||||
<FontAwesomeIcon icon="image" style={pal.link} size={24} />
|
<FontAwesomeIcon icon="image" style={pal.link} size={24} />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
{localPhotos.photos.map((item: PhotoIdentifier, index: number) => (
|
{isSetup &&
|
||||||
<TouchableOpacity
|
localPhotos.photos.map((item: PhotoIdentifier, index: number) => (
|
||||||
testID="openSelectPhotoButton"
|
<TouchableOpacity
|
||||||
key={`local-image-${index}`}
|
testID="openSelectPhotoButton"
|
||||||
style={[pal.border, styles.photoButton]}
|
key={`local-image-${index}`}
|
||||||
onPress={() => handleSelectPhoto(item)}>
|
style={[pal.border, styles.photoButton]}
|
||||||
<Image style={styles.photo} source={{uri: item.node.image.uri}} />
|
onPress={() => handleSelectPhoto(item)}>
|
||||||
</TouchableOpacity>
|
<Image style={styles.photo} source={{uri: item.node.image.uri}} />
|
||||||
))}
|
</TouchableOpacity>
|
||||||
|
))}
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,10 @@ import {
|
|||||||
openPicker,
|
openPicker,
|
||||||
Image as PickedImage,
|
Image as PickedImage,
|
||||||
} from 'react-native-image-crop-picker'
|
} from 'react-native-image-crop-picker'
|
||||||
|
import {
|
||||||
|
requestPhotoAccessIfNeeded,
|
||||||
|
requestCameraAccessIfNeeded,
|
||||||
|
} from '../../../lib/permissions'
|
||||||
import {colors, gradients} from '../../lib/styles'
|
import {colors, gradients} from '../../lib/styles'
|
||||||
import {DropdownButton} from './forms/DropdownButton'
|
import {DropdownButton} from './forms/DropdownButton'
|
||||||
import {usePalette} from '../../lib/hooks/usePalette'
|
import {usePalette} from '../../lib/hooks/usePalette'
|
||||||
@@ -53,26 +57,34 @@ export function UserAvatar({
|
|||||||
{
|
{
|
||||||
label: 'Camera',
|
label: 'Camera',
|
||||||
icon: 'camera',
|
icon: 'camera',
|
||||||
// TODO: dark mode icon
|
onPress: async () => {
|
||||||
onPress: () => {
|
if (!(await requestCameraAccessIfNeeded())) {
|
||||||
openCamera({
|
return
|
||||||
mediaType: 'photo',
|
}
|
||||||
cropping: true,
|
onSelectNewAvatar?.(
|
||||||
width: 2000,
|
await openCamera({
|
||||||
height: 2000,
|
mediaType: 'photo',
|
||||||
cropperCircleOverlay: true,
|
cropping: true,
|
||||||
forceJpg: true, // ios only
|
width: 2000,
|
||||||
compressImageQuality: 1,
|
height: 2000,
|
||||||
}).then(onSelectNewAvatar)
|
cropperCircleOverlay: true,
|
||||||
|
forceJpg: true, // ios only
|
||||||
|
compressImageQuality: 1,
|
||||||
|
}),
|
||||||
|
)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Library',
|
label: 'Library',
|
||||||
icon: 'image',
|
icon: 'image',
|
||||||
onPress: () => {
|
onPress: async () => {
|
||||||
openPicker({
|
if (!(await requestPhotoAccessIfNeeded())) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const item = await openPicker({
|
||||||
mediaType: 'photo',
|
mediaType: 'photo',
|
||||||
}).then(async item => {
|
})
|
||||||
|
onSelectNewAvatar?.(
|
||||||
await openCropper({
|
await openCropper({
|
||||||
mediaType: 'photo',
|
mediaType: 'photo',
|
||||||
path: item.path,
|
path: item.path,
|
||||||
@@ -81,8 +93,8 @@ export function UserAvatar({
|
|||||||
cropperCircleOverlay: true,
|
cropperCircleOverlay: true,
|
||||||
forceJpg: true, // ios only
|
forceJpg: true, // ios only
|
||||||
compressImageQuality: 1,
|
compressImageQuality: 1,
|
||||||
}).then(onSelectNewAvatar)
|
}),
|
||||||
})
|
)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// TODO: Remove avatar https://github.com/bluesky-social/social-app/issues/122
|
// TODO: Remove avatar https://github.com/bluesky-social/social-app/issues/122
|
||||||
|
|||||||
@@ -10,6 +10,10 @@ import {
|
|||||||
openCropper,
|
openCropper,
|
||||||
openPicker,
|
openPicker,
|
||||||
} from 'react-native-image-crop-picker'
|
} from 'react-native-image-crop-picker'
|
||||||
|
import {
|
||||||
|
requestPhotoAccessIfNeeded,
|
||||||
|
requestCameraAccessIfNeeded,
|
||||||
|
} from '../../../lib/permissions'
|
||||||
import {DropdownButton} from './forms/DropdownButton'
|
import {DropdownButton} from './forms/DropdownButton'
|
||||||
import {usePalette} from '../../lib/hooks/usePalette'
|
import {usePalette} from '../../lib/hooks/usePalette'
|
||||||
|
|
||||||
@@ -25,28 +29,36 @@ export function UserBanner({
|
|||||||
{
|
{
|
||||||
label: 'Camera',
|
label: 'Camera',
|
||||||
icon: 'camera',
|
icon: 'camera',
|
||||||
// TODO: Add darkmode support https://github.com/bluesky-social/social-app/issues/78
|
onPress: async () => {
|
||||||
onPress: () => {
|
if (!(await requestCameraAccessIfNeeded())) {
|
||||||
openCamera({
|
return
|
||||||
mediaType: 'photo',
|
}
|
||||||
cropping: true,
|
onSelectNewBanner?.(
|
||||||
compressImageMaxWidth: 6000,
|
await openCamera({
|
||||||
width: 6000,
|
mediaType: 'photo',
|
||||||
compressImageMaxHeight: 2000,
|
cropping: true,
|
||||||
height: 2000,
|
compressImageMaxWidth: 6000,
|
||||||
forceJpg: true, // ios only
|
width: 6000,
|
||||||
compressImageQuality: 1,
|
compressImageMaxHeight: 2000,
|
||||||
includeExif: true,
|
height: 2000,
|
||||||
}).then(onSelectNewBanner)
|
forceJpg: true, // ios only
|
||||||
|
compressImageQuality: 1,
|
||||||
|
includeExif: true,
|
||||||
|
}),
|
||||||
|
)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Library',
|
label: 'Library',
|
||||||
icon: 'image',
|
icon: 'image',
|
||||||
onPress: () => {
|
onPress: async () => {
|
||||||
openPicker({
|
if (!(await requestPhotoAccessIfNeeded())) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const item = await openPicker({
|
||||||
mediaType: 'photo',
|
mediaType: 'photo',
|
||||||
}).then(async item => {
|
})
|
||||||
|
onSelectNewBanner?.(
|
||||||
await openCropper({
|
await openCropper({
|
||||||
mediaType: 'photo',
|
mediaType: 'photo',
|
||||||
path: item.path,
|
path: item.path,
|
||||||
@@ -57,8 +69,8 @@ export function UserBanner({
|
|||||||
forceJpg: true, // ios only
|
forceJpg: true, // ios only
|
||||||
compressImageQuality: 1,
|
compressImageQuality: 1,
|
||||||
includeExif: true,
|
includeExif: true,
|
||||||
}).then(onSelectNewBanner)
|
}),
|
||||||
})
|
)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// TODO: Remove banner https://github.com/bluesky-social/social-app/issues/122
|
// TODO: Remove banner https://github.com/bluesky-social/social-app/issues/122
|
||||||
|
|||||||
@@ -11133,6 +11133,11 @@ react-native-pager-view@^6.0.2:
|
|||||||
resolved "https://registry.yarnpkg.com/react-native-pager-view/-/react-native-pager-view-6.1.2.tgz#3522079b9a9d6634ca5e8d153bc0b4d660254552"
|
resolved "https://registry.yarnpkg.com/react-native-pager-view/-/react-native-pager-view-6.1.2.tgz#3522079b9a9d6634ca5e8d153bc0b4d660254552"
|
||||||
integrity sha512-qs2KSFc+7N7B+UZ6SG2sTvCkppagm5fVyRclv1KFKc7lDtrhXLzN59tXJw575LDP/dRJoXsNwqUAhZJdws6ABQ==
|
integrity sha512-qs2KSFc+7N7B+UZ6SG2sTvCkppagm5fVyRclv1KFKc7lDtrhXLzN59tXJw575LDP/dRJoXsNwqUAhZJdws6ABQ==
|
||||||
|
|
||||||
|
react-native-permissions@^3.6.1:
|
||||||
|
version "3.6.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-native-permissions/-/react-native-permissions-3.6.1.tgz#73adcc1cef8cd57a9ef167b4507405f4ff5749c4"
|
||||||
|
integrity sha512-fzPpPQXeD34inUccqtoResSwYubfrwUguP4qrVUUv8+KSMjYSaHGoS5HaIJLZHlN9gO+TvLJZ2L5ZljTsb6qnQ==
|
||||||
|
|
||||||
react-native-progress@^5.0.0:
|
react-native-progress@^5.0.0:
|
||||||
version "5.0.0"
|
version "5.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/react-native-progress/-/react-native-progress-5.0.0.tgz#f5ac6ceaeee27f184c660b00f29419e82a9d0ab0"
|
resolved "https://registry.yarnpkg.com/react-native-progress/-/react-native-progress-5.0.0.tgz#f5ac6ceaeee27f184c660b00f29419e82a9d0ab0"
|
||||||
|
|||||||
Reference in New Issue
Block a user