diff --git a/src/analytics/features/types.ts b/src/analytics/features/types.ts index 2905969fc9..d851b00ead 100644 --- a/src/analytics/features/types.ts +++ b/src/analytics/features/types.ts @@ -9,6 +9,7 @@ export enum Features { ImportContactsOnboardingDisable = 'import_contacts:onboarding:disable', ImportContactsSettingsDisable = 'import_contacts:settings:disable', LiveNowBetaDisable = 'live_now_beta:disable', + ImageUploadsHighResolution = 'image_uploads:high_resolution', AATest = 'aa-test', } diff --git a/src/analytics/metrics/types.ts b/src/analytics/metrics/types.ts index 248965fd6d..ae1217df85 100644 --- a/src/analytics/metrics/types.ts +++ b/src/analytics/metrics/types.ts @@ -230,6 +230,9 @@ export type Events = { 'composer:gif:open': {} 'composer:gif:select': {} + 'composer:image:edit': { + platform: Platform['OS'] + } 'composerPrompt:press': {} 'composerPrompt:camera:press': {} 'composerPrompt:gallery:press': {} diff --git a/src/lib/api/index.ts b/src/lib/api/index.ts index 9eac9d6d4a..8755da5a23 100644 --- a/src/lib/api/index.ts +++ b/src/lib/api/index.ts @@ -51,10 +51,15 @@ interface PostOpts { langs?: string[] } +type FeatureFlags = { + highResolutionImages?: boolean +} + export async function post( agent: BskyAgent, queryClient: QueryClient, opts: PostOpts, + featureFlags?: FeatureFlags, ) { const thread = opts.thread opts.onStateChange?.(t`Processing...`) @@ -91,6 +96,7 @@ export async function post( queryClient, draft, opts.onStateChange, + featureFlags, ) let labels: $Typed | undefined if (draft.labels.length) { @@ -230,6 +236,7 @@ async function resolveEmbed( queryClient: QueryClient, draft: PostDraft, onStateChange: ((state: string) => void) | undefined, + featureFlags?: FeatureFlags, ): Promise< | $Typed | $Typed @@ -240,7 +247,13 @@ async function resolveEmbed( > { if (draft.embed.quote) { const [resolvedMedia, resolvedQuote] = await Promise.all([ - resolveMedia(agent, queryClient, draft.embed, onStateChange), + resolveMedia( + agent, + queryClient, + draft.embed, + onStateChange, + featureFlags, + ), resolveRecord(agent, queryClient, draft.embed.quote.uri), ]) if (resolvedMedia) { @@ -263,6 +276,7 @@ async function resolveEmbed( queryClient, draft.embed, onStateChange, + featureFlags, ) if (resolvedMedia) { return resolvedMedia @@ -288,6 +302,7 @@ async function resolveMedia( queryClient: QueryClient, embedDraft: EmbedDraft, onStateChange: ((state: string) => void) | undefined, + featureFlags?: FeatureFlags, ): Promise< | $Typed | $Typed @@ -303,7 +318,9 @@ 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, { + highResolution: featureFlags?.highResolutionImages, + }) logger.debug(`Uploading image #${i}`) const res = await uploadBlob(agent, path, mime) return { diff --git a/src/state/gallery.ts b/src/state/gallery.ts index a3e35ac26c..d6977817a2 100644 --- a/src/state/gallery.ts +++ b/src/state/gallery.ts @@ -200,10 +200,24 @@ export function resetImageManipulation( return img } -export async function compressImage(img: ComposerImage): Promise { +export async function compressImage( + img: ComposerImage, + options?: { + highResolution?: boolean + }, +): Promise { const source = img.transformed || img.source - const [w, h] = containImageRes(source.width, source.height, POST_IMG_MAX) + const [w, h] = containImageRes( + source.width, + source.height, + options?.highResolution + ? { + width: 4000, + height: 4000, + } + : POST_IMG_MAX, + ) let minQualityPercentage = 0 let maxQualityPercentage = 101 // exclusive diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx index 04d2145e10..2d076690d2 100644 --- a/src/view/com/composer/Composer.tsx +++ b/src/view/com/composer/Composer.tsx @@ -825,12 +825,21 @@ export const ComposePost = ({ try { logger.info(`composer: posting...`) postUri = ( - await apilib.post(agent, queryClient, { - thread, - replyTo: replyTo?.uri, - onStateChange: setPublishingStage, - langs: currentLanguages, - }) + await apilib.post( + agent, + queryClient, + { + thread, + replyTo: replyTo?.uri, + onStateChange: setPublishingStage, + langs: currentLanguages, + }, + { + highResolutionImages: ax.features.enabled( + ax.features.ImageUploadsHighResolution, + ), + }, + ) ).uris[0] /* diff --git a/src/view/com/composer/photos/Gallery.tsx b/src/view/com/composer/photos/Gallery.tsx index a99bdac394..2ad91fa594 100644 --- a/src/view/com/composer/photos/Gallery.tsx +++ b/src/view/com/composer/photos/Gallery.tsx @@ -4,6 +4,7 @@ import { type ImageStyle, Keyboard, type LayoutChangeEvent, + Platform, StyleSheet, TouchableOpacity, View, @@ -24,6 +25,7 @@ import {Admonition} from '#/components/Admonition' import * as Dialog from '#/components/Dialog' import {MediaInsetBorder} from '#/components/MediaInsetBorder' import {Text} from '#/components/Typography' +import {useAnalytics} from '#/analytics' import {IS_IOS, IS_NATIVE} from '#/env' import {type PostAction} from '../state/composer' import {EditImageDialog} from './EditImageDialog' @@ -147,6 +149,7 @@ const GalleryItem = ({ }: GalleryItemProps): React.ReactNode => { const {_} = useLingui() const t = useTheme() + const ax = useAnalytics() const altTextControl = Dialog.useDialogControl() const editControl = Dialog.useDialogControl() @@ -161,6 +164,10 @@ const GalleryItem = ({ } const onImageEdit = () => { + ax.metric('composer:image:edit', { + platform: Platform.OS, + }) + if (IS_NATIVE) { cropImage(image).then(next => { onChange(next)