From cadee015f383209a180a39a48b1581d2e0ffade3 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Mon, 16 Feb 2026 16:53:02 +0200 Subject: [PATCH] allow taking videos with camera button --- src/lib/media/picker.e2e.tsx | 18 ++++++++++-- src/lib/media/picker.tsx | 1 + src/lib/media/picker.web.tsx | 6 ++-- src/view/com/composer/Composer.tsx | 3 +- .../com/composer/photos/OpenCameraBtn.tsx | 28 ++++++++++++------- 5 files changed, 40 insertions(+), 16 deletions(-) diff --git a/src/lib/media/picker.e2e.tsx b/src/lib/media/picker.e2e.tsx index f01217d2a5..53298f6e59 100644 --- a/src/lib/media/picker.e2e.tsx +++ b/src/lib/media/picker.e2e.tsx @@ -3,7 +3,7 @@ import { getInfoAsync, readDirectoryAsync, } from 'expo-file-system/legacy' -import {type ImagePickerResult} from 'expo-image-picker' +import {type ImagePickerAsset, type ImagePickerResult} from 'expo-image-picker' import ExpoImageCropTool, { type OpenCropperOptions, } from '@bsky.app/expo-image-crop-tool' @@ -57,8 +57,20 @@ export async function openUnifiedPicker(): Promise { } } -export async function openCamera(): Promise { - return await getFile() +export async function openCamera(): Promise< + 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) { diff --git a/src/lib/media/picker.tsx b/src/lib/media/picker.tsx index 8284d73fb3..b4de6a2b32 100644 --- a/src/lib/media/picker.tsx +++ b/src/lib/media/picker.tsx @@ -29,6 +29,7 @@ export async function openCamera(customOpts: ImagePickerOptions) { size: asset.fileSize ?? 0, width: asset.width, height: asset.height, + asset, } } diff --git a/src/lib/media/picker.web.tsx b/src/lib/media/picker.web.tsx index 060169ff89..c489e488fd 100644 --- a/src/lib/media/picker.web.tsx +++ b/src/lib/media/picker.web.tsx @@ -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 PickerImage} from './picker.shared' -import {type CameraOpts} from './types' export {openPicker, openUnifiedPicker} from './picker.shared' -export async function openCamera(_opts: CameraOpts): Promise { +export async function openCamera( + _opts: ImagePickerOptions, +): Promise { throw new Error('openCamera is not supported on web') } diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx index ef54c4908f..6556287c47 100644 --- a/src/view/com/composer/Composer.tsx +++ b/src/view/com/composer/Composer.tsx @@ -1951,7 +1951,8 @@ function ComposerFooter({ /> onSelectVideo(post.id, asset)} /> {!isMobile ? ( diff --git a/src/view/com/composer/photos/OpenCameraBtn.tsx b/src/view/com/composer/photos/OpenCameraBtn.tsx index f917e4ee2e..0920f63e4e 100644 --- a/src/view/com/composer/photos/OpenCameraBtn.tsx +++ b/src/view/com/composer/photos/OpenCameraBtn.tsx @@ -1,9 +1,10 @@ import {useCallback} from 'react' +import {type ImagePickerAsset} from 'expo-image-picker' import * as MediaLibrary from 'expo-media-library' import {msg} from '@lingui/macro' 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 {openCamera} from '#/lib/media/picker' import {logger} from '#/logger' @@ -15,14 +16,15 @@ import {IS_NATIVE, IS_WEB_MOBILE} from '#/env' type Props = { 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 {requestCameraAccessIfNeeded} = useCameraPermission() const [mediaPermissionRes, requestMediaPermission] = - MediaLibrary.usePermissions({granularPermissions: ['photo']}) + MediaLibrary.usePermissions({granularPermissions: ['photo', 'video']}) const t = useTheme() const onPressTakePicture = useCallback(async () => { @@ -34,25 +36,31 @@ export function OpenCameraBtn({disabled, onAdd}: Props) { await requestMediaPermission() } - const img = await openCamera({ + const result = await openCamera({ + mediaTypes: ['images', 'videos'], 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 // the image even without these permissions if (mediaPermissionRes) { - await MediaLibrary.createAssetAsync(img.path) + await MediaLibrary.createAssetAsync(result.path) } - const res = await createComposerImage(img) - - onAdd([res]) + if (result.mime.startsWith('image')) { + const img = await createComposerImage(result) + onAddImage([img]) + } else if (result.mime.startsWith('video')) { + onAddVideo(result.asset) + } } catch (err: any) { // ignore logger.warn('Error using camera', {error: err}) } }, [ - onAdd, + onAddImage, + onAddVideo, requestCameraAccessIfNeeded, mediaPermissionRes, requestMediaPermission,