From 736acb8a612808f2372933c16a0f9f8f9766c1a9 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Tue, 2 Jun 2026 11:44:52 -0500 Subject: [PATCH] Rename image size constants and require them as params POST_IMG_MAX no longer described post images, so rename it to the semantically accurate pair: POST_IMG_MAX_SIZE (4000px/2MB, post images and pasted URLs) and PROFILE_IMAGES_MAX_SIZE (2000px/1MB, avatars, banners, onboarding, link thumbnails). Drop the constant defaults from compressImage, compressIfNeeded, and getResizedDimensions; callers now pass the appropriate max config object directly so the size policy lives at the call site. Web avatar/banner edits now use PROFILE_IMAGES_MAX_SIZE, matching the native profile path. Co-Authored-By: Claude Opus 4.8 (1M context) --- __tests__/lib/images.test.ts | 21 +++++++++++++++---- src/lib/api/index.ts | 6 +++++- src/lib/api/resolve.ts | 6 +++--- src/lib/constants.ts | 14 ++++++------- src/lib/media/manip.ts | 9 ++++---- src/lib/media/manip.web.ts | 9 ++++---- src/lib/media/picker.e2e.tsx | 18 +++++++++------- src/lib/media/util.ts | 4 +--- src/screens/Onboarding/StepProfile/index.tsx | 3 ++- src/state/gallery.ts | 13 +++++------- .../com/composer/photos/OpenCameraBtn.tsx | 4 ++-- .../com/composer/text-input/TextInput.tsx | 6 +++--- src/view/com/util/UserAvatar.tsx | 5 ++++- src/view/com/util/UserBanner.tsx | 5 ++++- 14 files changed, 72 insertions(+), 51 deletions(-) diff --git a/__tests__/lib/images.test.ts b/__tests__/lib/images.test.ts index 6723caa5e3..1319d5c7c8 100644 --- a/__tests__/lib/images.test.ts +++ b/__tests__/lib/images.test.ts @@ -1,6 +1,7 @@ import {createDownloadResumable, deleteAsync} from 'expo-file-system/legacy' import {manipulateAsync, SaveFormat} from 'expo-image-manipulator' +import {PROFILE_IMAGES_MAX_SIZE} from '../../src/lib/constants' import { downloadAndResize, type DownloadAndResizeOpts, @@ -88,13 +89,19 @@ describe('downloadAndResize', () => { width: 1200, height: 1000, } - const resizedDimensionsOne = getResizedDimensions(initialDimensionsOne) + const resizedDimensionsOne = getResizedDimensions( + initialDimensionsOne, + PROFILE_IMAGES_MAX_SIZE, + ) const initialDimensionsTwo = { width: 1000, height: 1200, } - const resizedDimensionsTwo = getResizedDimensions(initialDimensionsTwo) + const resizedDimensionsTwo = getResizedDimensions( + initialDimensionsTwo, + PROFILE_IMAGES_MAX_SIZE, + ) expect(resizedDimensionsOne).toEqual(initialDimensionsOne) expect(resizedDimensionsTwo).toEqual(initialDimensionsTwo) @@ -105,13 +112,19 @@ describe('downloadAndResize', () => { width: 3000, height: 1500, } - const resizedDimensionsOne = getResizedDimensions(initialDimensionsOne) + const resizedDimensionsOne = getResizedDimensions( + initialDimensionsOne, + PROFILE_IMAGES_MAX_SIZE, + ) const initialDimensionsTwo = { width: 2000, height: 4000, } - const resizedDimensionsTwo = getResizedDimensions(initialDimensionsTwo) + const resizedDimensionsTwo = getResizedDimensions( + initialDimensionsTwo, + PROFILE_IMAGES_MAX_SIZE, + ) expect(resizedDimensionsOne).toEqual({ width: 2000, diff --git a/src/lib/api/index.ts b/src/lib/api/index.ts index d9a026ca83..6489a78743 100644 --- a/src/lib/api/index.ts +++ b/src/lib/api/index.ts @@ -21,6 +21,7 @@ import {sha256} from 'js-sha256' import {CID} from 'multiformats/cid' import * as Hasher from 'multiformats/hashes/hasher' +import {POST_IMG_MAX_SIZE} from '#/lib/constants' import {isNetworkError} from '#/lib/strings/errors' import {shortenLinks, stripInvalidMentions} from '#/lib/strings/rich-text-manip' import {logger} from '#/logger' @@ -323,7 +324,10 @@ async function resolveMedia( const images: AppBskyEmbedImages.Image[] = await Promise.all( imagesDraft.map(async (image, i) => { logger.debug(`Compressing image #${i}`) - const {path, width, height, mime} = await compressImage(image) + const {path, width, height, mime} = await compressImage( + image, + POST_IMG_MAX_SIZE, + ) logger.debug(`Uploading image #${i}`) const res = await uploadBlob(agent, path, mime) return { diff --git a/src/lib/api/resolve.ts b/src/lib/api/resolve.ts index d9d7d326cb..dad0a5b9f3 100644 --- a/src/lib/api/resolve.ts +++ b/src/lib/api/resolve.ts @@ -6,7 +6,7 @@ import { } from '@atproto/api' import {AtUri} from '@atproto/api' -import {POST_IMG_MAX} from '#/lib/constants' +import {PROFILE_IMAGES_MAX_SIZE} from '#/lib/constants' import {getLinkMeta, type LinkMeta} from '#/lib/link-meta/link-meta' import {resolveShortLink} from '#/lib/link-meta/resolve-short-link' import {downloadAndResize} from '#/lib/media/manip' @@ -257,8 +257,8 @@ export async function imageToThumb( try { const img = await downloadAndResize({ uri: imageUri, - maxDimension: POST_IMG_MAX.width, - maxSize: POST_IMG_MAX.size, + maxDimension: PROFILE_IMAGES_MAX_SIZE.width, + maxSize: PROFILE_IMAGES_MAX_SIZE.size, timeout: 15e3, }) if (img) { diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 5d79eb62c1..bd8bb70120 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -96,18 +96,18 @@ export const STAGING_FEEDS = [ `feedgen|${STAGING_DEFAULT_FEED('thevids')}`, ] -export const POST_IMG_MAX = { - width: 2000, - height: 2000, - size: 1000000, -} - -export const POST_IMG_MAX_HIGH_RES = { +export const POST_IMG_MAX_SIZE = { width: 4000, height: 4000, size: 2000000, } +export const PROFILE_IMAGES_MAX_SIZE = { + width: 2000, + height: 2000, + size: 1000000, +} + export const STAGING_LINK_META_PROXY = 'https://cardyb.staging.bsky.dev/v1/extract?url=' diff --git a/src/lib/media/manip.ts b/src/lib/media/manip.ts index 0a6f6402e1..f004405943 100644 --- a/src/lib/media/manip.ts +++ b/src/lib/media/manip.ts @@ -16,7 +16,6 @@ import {manipulateAsync, SaveFormat} from 'expo-image-manipulator' import * as MediaLibrary from 'expo-media-library' import * as Sharing from 'expo-sharing' -import {POST_IMG_MAX} from '#/lib/constants' import {logger} from '#/logger' import {IS_ANDROID, IS_IOS} from '#/env' import {type PickerImage} from './picker.shared' @@ -25,14 +24,14 @@ import {convertCdnPreset, getResizedDimensions} from './util' export async function compressIfNeeded( img: PickerImage, - maxSize: number = POST_IMG_MAX.size, + max: {width: number; size: number}, ): Promise { - if (img.size < maxSize) { + if (img.size < max.size) { return img } const resizedImage = await doResize(normalizePath(img.path), { - maxDimension: POST_IMG_MAX.width, - maxSize, + maxDimension: max.width, + maxSize: max.size, }) const finalImageMovedPath = await moveToPermanentPath( resizedImage.path, diff --git a/src/lib/media/manip.web.ts b/src/lib/media/manip.web.ts index 948482fa6e..084bf74c33 100644 --- a/src/lib/media/manip.web.ts +++ b/src/lib/media/manip.web.ts @@ -1,4 +1,3 @@ -import {POST_IMG_MAX} from '#/lib/constants' import {type PickerImage} from './picker.shared' import {type Dimensions} from './types' import { @@ -10,14 +9,14 @@ import { export async function compressIfNeeded( img: PickerImage, - maxSize: number, + max: {width: number; size: number}, ): Promise { - if (img.size < maxSize) { + if (img.size < max.size) { return img } return await doResize(img.path, { - maxDimension: POST_IMG_MAX.width, - maxSize, + maxDimension: max.width, + maxSize: max.size, }) } diff --git a/src/lib/media/picker.e2e.tsx b/src/lib/media/picker.e2e.tsx index f01217d2a5..97a1c7b7ad 100644 --- a/src/lib/media/picker.e2e.tsx +++ b/src/lib/media/picker.e2e.tsx @@ -8,6 +8,7 @@ import ExpoImageCropTool, { type OpenCropperOptions, } from '@bsky.app/expo-image-crop-tool' +import {PROFILE_IMAGES_MAX_SIZE} from '#/lib/constants' import {compressIfNeeded} from './manip' import {type PickerImage} from './picker.shared' @@ -28,13 +29,16 @@ async function getFile() { throw new Error('Failed to get file info') } - return await compressIfNeeded({ - path: file, - mime: 'image/jpeg', - size: fileInfo.size, - width: 4288, - height: 2848, - }) + return await compressIfNeeded( + { + path: file, + mime: 'image/jpeg', + size: fileInfo.size, + width: 4288, + height: 2848, + }, + PROFILE_IMAGES_MAX_SIZE, + ) } export async function openPicker(): Promise { diff --git a/src/lib/media/util.ts b/src/lib/media/util.ts index ebe40034a6..05163171ca 100644 --- a/src/lib/media/util.ts +++ b/src/lib/media/util.ts @@ -1,5 +1,3 @@ -import {POST_IMG_MAX} from '#/lib/constants' - export function extractDataUriMime(uri: string): string { return uri.substring(uri.indexOf(':') + 1, uri.indexOf(';')) } @@ -9,7 +7,7 @@ export function getResizedDimensions( width: number height: number }, - max: {width: number; height: number} = POST_IMG_MAX, + max: {width: number; height: number}, ) { if (originalDims.width <= max.width && originalDims.height <= max.height) { return originalDims diff --git a/src/screens/Onboarding/StepProfile/index.tsx b/src/screens/Onboarding/StepProfile/index.tsx index 6c84ec4c86..5e3badf03d 100644 --- a/src/screens/Onboarding/StepProfile/index.tsx +++ b/src/screens/Onboarding/StepProfile/index.tsx @@ -19,6 +19,7 @@ import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {Trans} from '@lingui/react/macro' +import {PROFILE_IMAGES_MAX_SIZE} from '#/lib/constants' import {usePhotoLibraryPermission} from '#/lib/hooks/usePermissions' import {compressIfNeeded} from '#/lib/media/manip' import {openCropper} from '#/lib/media/picker' @@ -212,7 +213,7 @@ export function StepProfile() { } } } - image = await compressIfNeeded(image, 1000000) + image = await compressIfNeeded(image, PROFILE_IMAGES_MAX_SIZE) // If we are on mobile, prefetching the image will load the image into memory before we try and display it, // stopping any brief flickers. diff --git a/src/state/gallery.ts b/src/state/gallery.ts index fff0b686f5..337604823e 100644 --- a/src/state/gallery.ts +++ b/src/state/gallery.ts @@ -13,7 +13,6 @@ import { } from 'expo-image-manipulator' import {nanoid} from 'nanoid/non-secure' -import {POST_IMG_MAX_HIGH_RES} from '#/lib/constants' import {getImageDim} from '#/lib/media/manip' import {openCropper} from '#/lib/media/picker' import {type PickerImage} from '#/lib/media/picker.shared' @@ -204,17 +203,15 @@ export function resetImageManipulation( export async function compressImage( img: ComposerImage, - { - maxDimension = POST_IMG_MAX_HIGH_RES.width, - maxBytes = POST_IMG_MAX_HIGH_RES.size, - }: {maxDimension?: number; maxBytes?: number} = {}, + max: {width: number; size: number}, ): Promise { const source = img.transformed || img.source let attempts = 0 - // Seeded from `maxDimension` but shrunk per attempt below, so keep the param - // itself pristine. - let currentDimension = maxDimension + // Seeded from `max.width` but shrunk per attempt below, so keep the passed-in + // value pristine. + let currentDimension = max.width + const maxBytes = max.size let minQualityPercentage = 0 let maxQualityPercentage = 101 // exclusive diff --git a/src/view/com/composer/photos/OpenCameraBtn.tsx b/src/view/com/composer/photos/OpenCameraBtn.tsx index 2df285f7ae..7b3faff053 100644 --- a/src/view/com/composer/photos/OpenCameraBtn.tsx +++ b/src/view/com/composer/photos/OpenCameraBtn.tsx @@ -3,7 +3,7 @@ import * as MediaLibrary from 'expo-media-library' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' -import {POST_IMG_MAX} from '#/lib/constants' +import {POST_IMG_MAX_SIZE} from '#/lib/constants' import {useCameraPermission} from '#/lib/hooks/usePermissions' import {openCamera} from '#/lib/media/picker' import {logger} from '#/logger' @@ -35,7 +35,7 @@ export function OpenCameraBtn({disabled, onAdd}: Props) { } const img = await openCamera({ - aspect: [POST_IMG_MAX.width, POST_IMG_MAX.height], + aspect: [POST_IMG_MAX_SIZE.width, POST_IMG_MAX_SIZE.height], }) // If we don't have permissions it's fine, we just wont save it. The post itself will still have access to diff --git a/src/view/com/composer/text-input/TextInput.tsx b/src/view/com/composer/text-input/TextInput.tsx index 958d57a55d..98e13c7620 100644 --- a/src/view/com/composer/text-input/TextInput.tsx +++ b/src/view/com/composer/text-input/TextInput.tsx @@ -16,7 +16,7 @@ import {type PasteEventPayload, TextInputWrapper} from 'expo-paste-input' import {AppBskyRichtextFacet, RichText} from '@atproto/api' import {useLingui} from '@lingui/react/macro' -import {POST_IMG_MAX_HIGH_RES} from '#/lib/constants' +import {POST_IMG_MAX_SIZE} from '#/lib/constants' import {downloadAndResize} from '#/lib/media/manip' import {isUriImage} from '#/lib/media/util' import {getMentionAt, insertMentionAt} from '#/lib/strings/mention-manip' @@ -93,8 +93,8 @@ export function TextInput({ if (isUriImage(feature.uri)) { const res = await downloadAndResize({ uri: feature.uri, - maxDimension: POST_IMG_MAX_HIGH_RES.width, - maxSize: POST_IMG_MAX_HIGH_RES.size, + maxDimension: POST_IMG_MAX_SIZE.width, + maxSize: POST_IMG_MAX_SIZE.size, timeout: 15e3, }) diff --git a/src/view/com/util/UserAvatar.tsx b/src/view/com/util/UserAvatar.tsx index 420fd0270f..bab4c62348 100644 --- a/src/view/com/util/UserAvatar.tsx +++ b/src/view/com/util/UserAvatar.tsx @@ -17,6 +17,7 @@ import {useLingui} from '@lingui/react' import {Trans} from '@lingui/react/macro' import {useQueryClient} from '@tanstack/react-query' +import {PROFILE_IMAGES_MAX_SIZE} from '#/lib/constants' import {useHaptics} from '#/lib/haptics' import { useCameraPermission, @@ -394,6 +395,7 @@ let EditableUserAvatar = ({ await openCamera({ aspect: [1, 1], }), + PROFILE_IMAGES_MAX_SIZE, ), ) }, [onSelectNewAvatar, requestCameraAccessIfNeeded]) @@ -422,6 +424,7 @@ let EditableUserAvatar = ({ shape: circular ? 'circle' : 'rectangle', aspectRatio: 1, }), + PROFILE_IMAGES_MAX_SIZE, ), ) } else { @@ -448,7 +451,7 @@ let EditableUserAvatar = ({ const onChangeEditImage = useCallback( async (image: ComposerImage) => { - const compressed = await compressImage(image) + const compressed = await compressImage(image, PROFILE_IMAGES_MAX_SIZE) onSelectNewAvatar(compressed) }, [onSelectNewAvatar], diff --git a/src/view/com/util/UserBanner.tsx b/src/view/com/util/UserBanner.tsx index 8146b7ad9d..c44c30c0c3 100644 --- a/src/view/com/util/UserBanner.tsx +++ b/src/view/com/util/UserBanner.tsx @@ -6,6 +6,7 @@ import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {Trans} from '@lingui/react/macro' +import {PROFILE_IMAGES_MAX_SIZE} from '#/lib/constants' import { useCameraPermission, usePhotoLibraryPermission, @@ -62,6 +63,7 @@ export function UserBanner({ await openCamera({ aspect: [3, 1], }), + PROFILE_IMAGES_MAX_SIZE, ), ) }, [onSelectNewBanner, requestCameraAccessIfNeeded]) @@ -83,6 +85,7 @@ export function UserBanner({ imageUri: items[0].path, aspectRatio: 3 / 1, }), + PROFILE_IMAGES_MAX_SIZE, ), ) } else { @@ -108,7 +111,7 @@ export function UserBanner({ const onChangeEditImage = useCallback( async (image: ComposerImage) => { - const compressed = await compressImage(image) + const compressed = await compressImage(image, PROFILE_IMAGES_MAX_SIZE) onSelectNewBanner?.(compressed) }, [onSelectNewBanner],