allow taking videos with camera button

This commit is contained in:
Samuel Newman
2026-02-16 16:53:02 +02:00
parent 79e27e303b
commit cadee015f3
5 changed files with 40 additions and 16 deletions
+15 -3
View File
@@ -3,7 +3,7 @@ import {
getInfoAsync, getInfoAsync,
readDirectoryAsync, readDirectoryAsync,
} from 'expo-file-system/legacy' } from 'expo-file-system/legacy'
import {type ImagePickerResult} from 'expo-image-picker' import {type ImagePickerAsset, type ImagePickerResult} from 'expo-image-picker'
import ExpoImageCropTool, { import ExpoImageCropTool, {
type OpenCropperOptions, type OpenCropperOptions,
} from '@bsky.app/expo-image-crop-tool' } from '@bsky.app/expo-image-crop-tool'
@@ -57,8 +57,20 @@ export async function openUnifiedPicker(): Promise<ImagePickerResult> {
} }
} }
export async function openCamera(): Promise<PickerImage> { export async function openCamera(): Promise<
return await getFile() PickerImage & {asset: ImagePickerAsset}
> {
const file = await getFile()
return {
...file,
asset: {
uri: file.path,
width: file.width,
height: file.height,
mimeType: file.mime,
type: 'image',
},
}
} }
export async function openCropper(opts: OpenCropperOptions) { export async function openCropper(opts: OpenCropperOptions) {
+1
View File
@@ -29,6 +29,7 @@ export async function openCamera(customOpts: ImagePickerOptions) {
size: asset.fileSize ?? 0, size: asset.fileSize ?? 0,
width: asset.width, width: asset.width,
height: asset.height, height: asset.height,
asset,
} }
} }
+4 -2
View File
@@ -1,11 +1,13 @@
import {type ImagePickerAsset, type ImagePickerOptions} from 'expo-image-picker'
import {type OpenCropperOptions} from '@bsky.app/expo-image-crop-tool' import {type OpenCropperOptions} from '@bsky.app/expo-image-crop-tool'
import {type PickerImage} from './picker.shared' import {type PickerImage} from './picker.shared'
import {type CameraOpts} from './types'
export {openPicker, openUnifiedPicker} from './picker.shared' export {openPicker, openUnifiedPicker} from './picker.shared'
export async function openCamera(_opts: CameraOpts): Promise<PickerImage> { export async function openCamera(
_opts: ImagePickerOptions,
): Promise<PickerImage & {asset: ImagePickerAsset}> {
throw new Error('openCamera is not supported on web') throw new Error('openCamera is not supported on web')
} }
+2 -1
View File
@@ -1951,7 +1951,8 @@ function ComposerFooter({
/> />
<OpenCameraBtn <OpenCameraBtn
disabled={media?.type === 'images' ? isMaxImages : !!media} disabled={media?.type === 'images' ? isMaxImages : !!media}
onAdd={onImageAdd} onAddImage={onImageAdd}
onAddVideo={asset => onSelectVideo(post.id, asset)}
/> />
<SelectGifBtn onSelectGif={onSelectGif} disabled={!!media} /> <SelectGifBtn onSelectGif={onSelectGif} disabled={!!media} />
{!isMobile ? ( {!isMobile ? (
+18 -10
View File
@@ -1,9 +1,10 @@
import {useCallback} from 'react' import {useCallback} from 'react'
import {type ImagePickerAsset} from 'expo-image-picker'
import * as MediaLibrary from 'expo-media-library' import * as MediaLibrary from 'expo-media-library'
import {msg} from '@lingui/macro' import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {POST_IMG_MAX} from '#/lib/constants' import {POST_IMG_MAX, VIDEO_MAX_DURATION_MS} from '#/lib/constants'
import {useCameraPermission} from '#/lib/hooks/usePermissions' import {useCameraPermission} from '#/lib/hooks/usePermissions'
import {openCamera} from '#/lib/media/picker' import {openCamera} from '#/lib/media/picker'
import {logger} from '#/logger' import {logger} from '#/logger'
@@ -15,14 +16,15 @@ import {IS_NATIVE, IS_WEB_MOBILE} from '#/env'
type Props = { type Props = {
disabled?: boolean disabled?: boolean
onAdd: (next: ComposerImage[]) => void onAddImage: (next: ComposerImage[]) => void
onAddVideo: (asset: ImagePickerAsset) => void
} }
export function OpenCameraBtn({disabled, onAdd}: Props) { export function OpenCameraBtn({disabled, onAddImage, onAddVideo}: Props) {
const {_} = useLingui() const {_} = useLingui()
const {requestCameraAccessIfNeeded} = useCameraPermission() const {requestCameraAccessIfNeeded} = useCameraPermission()
const [mediaPermissionRes, requestMediaPermission] = const [mediaPermissionRes, requestMediaPermission] =
MediaLibrary.usePermissions({granularPermissions: ['photo']}) MediaLibrary.usePermissions({granularPermissions: ['photo', 'video']})
const t = useTheme() const t = useTheme()
const onPressTakePicture = useCallback(async () => { const onPressTakePicture = useCallback(async () => {
@@ -34,25 +36,31 @@ export function OpenCameraBtn({disabled, onAdd}: Props) {
await requestMediaPermission() await requestMediaPermission()
} }
const img = await openCamera({ const result = await openCamera({
mediaTypes: ['images', 'videos'],
aspect: [POST_IMG_MAX.width, POST_IMG_MAX.height], aspect: [POST_IMG_MAX.width, POST_IMG_MAX.height],
videoMaxDuration: VIDEO_MAX_DURATION_MS / 1000,
}) })
// If we don't have permissions it's fine, we just wont save it. The post itself will still have access to // If we don't have permissions it's fine, we just wont save it. The post itself will still have access to
// the image even without these permissions // the image even without these permissions
if (mediaPermissionRes) { if (mediaPermissionRes) {
await MediaLibrary.createAssetAsync(img.path) await MediaLibrary.createAssetAsync(result.path)
} }
const res = await createComposerImage(img) if (result.mime.startsWith('image')) {
const img = await createComposerImage(result)
onAdd([res]) onAddImage([img])
} else if (result.mime.startsWith('video')) {
onAddVideo(result.asset)
}
} catch (err: any) { } catch (err: any) {
// ignore // ignore
logger.warn('Error using camera', {error: err}) logger.warn('Error using camera', {error: err})
} }
}, [ }, [
onAdd, onAddImage,
onAddVideo,
requestCameraAccessIfNeeded, requestCameraAccessIfNeeded,
mediaPermissionRes, mediaPermissionRes,
requestMediaPermission, requestMediaPermission,