From 122a46891a7f912c8e2777bae00c4b1f64154257 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Mon, 18 Aug 2025 18:28:01 -0500 Subject: [PATCH 1/6] [APP-1318] `SelectMediaButton` (#8828) * Integrate Sonner for toasts * Fix animation on iOS * Refactor API * Update e2e file * [APP-1318] Post composer: combine image & video buttons (#8710) * add: select media btn * udpate: compose post with combined image and video support * add: video combine button with edge cases * add select media btn * test: select media btn * add: media button update * remove unused files and update toast on android * update: make strings shorter * add: ValidatedVideoAsset type * update link comments and add toast support for native and web * rebase latest toast and update toast structure * remove unused prop * fix types * undo changes to yarn.lock * remove: support for mkv files * update: eslint and prettier (cherry picked from commit f69779ee130f07e1c49219b53117e3bdd1a9f81b) * Add missing props to launchImageLibraryAsync (cherry picked from commit 2e80ae561fd66850f787cac0aae0fa5a6980f8f5) * Rough out new approach (cherry picked from commit 9add225160e7e407befc73e9cdd9743a30cdf1cd) * Comments and cleanup (cherry picked from commit e69bd186e7335372f440c446ae6643ed0fb15db9) * Handle native case (cherry picked from commit 74e38acdfd9181d0557426691fcbcbf0800481ca) * Refactor (cherry picked from commit 68aea496db8df54dba5f58da267ad962c28ef995) * Rename (cherry picked from commit 8609e59ad14219e7378ee6cb9514d633ce7efc27) * Cleanup, comments (cherry picked from commit 6c9c98648e37257285a9c8caeb1eadcc56c81402) * Rename (cherry picked from commit 66e3db539d5baa41436c9e49af06e87a78e9e7e1) * Handle selectionLimit on Android (cherry picked from commit 251f06dd5e65a7083b810bad3d81114b2fe9ab39) * create composer images in parallel (cherry picked from commit 70ea79d9d76d99e9c99a7d2296caed84c718650e) * Update toast API usage (cherry picked from commit e370018b8ed8cdfd7675c9634058c72cb59d39de) * Ensure once one type of media is selected, you can only select more of that type (cherry picked from commit 1a9e6e0cdb5234667f08e3dd9107ae598941fc23) * Remove TODO and debug code * Add more descriptive a11y label to button Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> * Add back post success toast * Include mimeType in toast error * Remove unneeded toast * Clarify hint * Typo Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> * allow gifs on native, just treat as images * disable haptic toast * allow gifs on native, treat as videos * only do keyboard dismiss on native * tweak pasting logic * hide web scrubber in certain situations * Update MaxImages translation Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> * Add plural formatting to a11y hint translation Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> * fix suggestion * Protect against no valid assets selected * Handle conversion of too-big assets on web * Reorder * Bump expo-image-picker to include bug/perf improvements See https://github.com/expo/expo/blob/main/packages/expo-image-picker/CHANGELOG.md#1700--2025-08-13 * Handle edge case validations * Ok actually bump expo-image-picker * Comment * HEIC support Android * Fix handling for new picker version, improve size validation * Remove getVideoMetadata handling, no longer needed * Handle web video duration * Update src/view/com/composer/SelectMediaButton.tsx Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> --------- Co-authored-by: Anastasiya Uraleva Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> Co-authored-by: Samuel Newman --- .gitignore | 1 + package.json | 2 +- patches/expo-image-picker+16.1.4.patch | 38 -- patches/expo-image-picker+16.1.4.patch.md | 5 - .../VideoEmbedInner/web-controls/Scrubber.tsx | 2 + .../web-controls/VideoControls.tsx | 16 +- src/lib/constants.ts | 4 + src/lib/haptics.ts | 4 +- src/lib/media/picker.shared.ts | 6 +- src/lib/media/video/compress.ts | 13 +- src/view/com/composer/Composer.tsx | 268 +++++---- src/view/com/composer/SelectMediaButton.tsx | 524 ++++++++++++++++++ .../composer/photos/ImageAltTextDialog.tsx | 5 +- .../com/composer/photos/SelectPhotoBtn.tsx | 60 -- .../com/composer/videos/SelectVideoBtn.tsx | 88 --- src/view/com/composer/videos/VideoPreview.tsx | 31 +- yarn.lock | 15 +- 17 files changed, 763 insertions(+), 319 deletions(-) delete mode 100644 patches/expo-image-picker+16.1.4.patch delete mode 100644 patches/expo-image-picker+16.1.4.patch.md create mode 100644 src/view/com/composer/SelectMediaButton.tsx delete mode 100644 src/view/com/composer/photos/SelectPhotoBtn.tsx delete mode 100644 src/view/com/composer/videos/SelectVideoBtn.tsx diff --git a/.gitignore b/.gitignore index 7233b35472..0bef5d88a1 100644 --- a/.gitignore +++ b/.gitignore @@ -58,6 +58,7 @@ buck-out/ # Ruby / CocoaPods /ios/Pods/ /vendor/bundle/ +Gemfile.lock # Testing coverage/ diff --git a/package.json b/package.json index ee5d276f4a..0f3f685713 100644 --- a/package.json +++ b/package.json @@ -146,7 +146,7 @@ "expo-image": "^2.4.0", "expo-image-crop-tool": "^0.1.8", "expo-image-manipulator": "~13.1.7", - "expo-image-picker": "~16.1.4", + "expo-image-picker": "^17.0.2", "expo-intent-launcher": "^12.1.5", "expo-linear-gradient": "~14.1.5", "expo-linking": "~7.1.5", diff --git a/patches/expo-image-picker+16.1.4.patch b/patches/expo-image-picker+16.1.4.patch deleted file mode 100644 index 0396fecbc1..0000000000 --- a/patches/expo-image-picker+16.1.4.patch +++ /dev/null @@ -1,38 +0,0 @@ -diff --git a/node_modules/expo-image-picker/android/src/main/java/expo/modules/imagepicker/MediaHandler.kt b/node_modules/expo-image-picker/android/src/main/java/expo/modules/imagepicker/MediaHandler.kt -index c863fb8..cde8859 100644 ---- a/node_modules/expo-image-picker/android/src/main/java/expo/modules/imagepicker/MediaHandler.kt -+++ b/node_modules/expo-image-picker/android/src/main/java/expo/modules/imagepicker/MediaHandler.kt -@@ -101,16 +101,30 @@ internal class MediaHandler( - val fileData = getAdditionalFileData(sourceUri) - val mimeType = getType(context.contentResolver, sourceUri) - -+ // Extract basic metadata -+ var width = metadataRetriever.extractInt(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH) -+ var height = metadataRetriever.extractInt(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT) -+ val rotation = metadataRetriever.extractInt(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION) -+ -+ // Android returns the encoded width/height which do not take the display rotation into -+ // account. For videos recorded in portrait mode the encoded dimensions are often landscape -+ // (e.g. 1920x1080) paired with a 90°/270° rotation flag. iOS adjusts these values before -+ // reporting them, so to keep the behaviour consistent across platforms we swap the width -+ // and height when the rotation indicates the video should be displayed in portrait. -+ if (rotation % 180 != 0) { -+ width = height.also { height = width } -+ } -+ - return ImagePickerAsset( - type = MediaType.VIDEO, - uri = outputUri.toString(), -- width = metadataRetriever.extractInt(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH), -- height = metadataRetriever.extractInt(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT), -+ width = width, -+ height = height, - fileName = fileData?.fileName, - fileSize = fileData?.fileSize, - mimeType = mimeType, - duration = metadataRetriever.extractInt(MediaMetadataRetriever.METADATA_KEY_DURATION), -- rotation = metadataRetriever.extractInt(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION), -+ rotation = rotation, - assetId = sourceUri.getMediaStoreAssetId() - ) - } catch (cause: FailedToExtractVideoMetadataException) { diff --git a/patches/expo-image-picker+16.1.4.patch.md b/patches/expo-image-picker+16.1.4.patch.md deleted file mode 100644 index 7855e86212..0000000000 --- a/patches/expo-image-picker+16.1.4.patch.md +++ /dev/null @@ -1,5 +0,0 @@ -# Expo Image Picker patch - -Cherry-picked https://github.com/expo/expo/pull/37849 - -Remove when we update to a version that includes this commit. diff --git a/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/Scrubber.tsx b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/Scrubber.tsx index d84a90fa62..e4814462f7 100644 --- a/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/Scrubber.tsx +++ b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/Scrubber.tsx @@ -146,6 +146,8 @@ export function Scrubber({ const progress = scrubberActive ? seekPosition : currentTime const progressPercent = (progress / duration) * 100 + if (duration < 3) return null + return ( - - {formatTime(currentTime)} / {formatTime(duration)} - + {Math.round(duration) > 0 && ( + + {formatTime(currentTime)} / {formatTime(duration)} + + )} {hasSubtitleTrack && ( 4) { - Toast.show(t`You may only select up to 4 images`, 'exclamation-circle') - } - return (response.assets ?? []) - .slice(0, 4) .filter(asset => { if (asset.mimeType?.startsWith('image/')) return true Toast.show(t`Only image files are supported`, 'exclamation-circle') diff --git a/src/lib/media/video/compress.ts b/src/lib/media/video/compress.ts index c2d1470c63..1d00bfcea1 100644 --- a/src/lib/media/video/compress.ts +++ b/src/lib/media/video/compress.ts @@ -1,8 +1,8 @@ import {getVideoMetaData, Video} from 'react-native-compressor' -import {ImagePickerAsset} from 'expo-image-picker' +import {type ImagePickerAsset} from 'expo-image-picker' -import {SUPPORTED_MIME_TYPES, SupportedMimeTypes} from '#/lib/constants' -import {CompressedVideo} from './types' +import {SUPPORTED_MIME_TYPES, type SupportedMimeTypes} from '#/lib/constants' +import {type CompressedVideo} from './types' import {extToMime} from './util' const MIN_SIZE_FOR_COMPRESSION = 25 // 25mb @@ -20,6 +20,13 @@ export async function compressVideo( file.mimeType as SupportedMimeTypes, ) + if (file.mimeType === 'image/gif') { + // let's hope they're small enough that they don't need compression! + // this compression library doesn't support gifs + // worst case - server rejects them. I think that's fine -sfn + return {uri: file.uri, size: file.fileSize ?? -1, mimeType: 'image/gif'} + } + const minimumFileSizeForCompress = isAcceptableFormat ? MIN_SIZE_FOR_COMPRESSION : 0 diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx index 2965453530..c3e0526b9a 100644 --- a/src/view/com/composer/Composer.tsx +++ b/src/view/com/composer/Composer.tsx @@ -40,6 +40,7 @@ import Animated, { ZoomIn, ZoomOut, } from 'react-native-reanimated' +import {RootSiblingParent} from 'react-native-root-siblings' import {useSafeAreaInsets} from 'react-native-safe-area-context' import {type ImagePickerAsset} from 'expo-image-picker' import { @@ -77,7 +78,11 @@ import {logger} from '#/logger' import {isAndroid, isIOS, isNative, isWeb} from '#/platform/detection' import {useDialogStateControlContext} from '#/state/dialogs' import {emitPostCreated} from '#/state/events' -import {type ComposerImage, pasteImage} from '#/state/gallery' +import { + type ComposerImage, + createComposerImage, + pasteImage, +} from '#/state/gallery' import {useModalControls} from '#/state/modals' import {useRequireAltTextEnabled} from '#/state/preferences' import { @@ -103,7 +108,6 @@ import {LabelsBtn} from '#/view/com/composer/labels/LabelsBtn' import {Gallery} from '#/view/com/composer/photos/Gallery' import {OpenCameraBtn} from '#/view/com/composer/photos/OpenCameraBtn' import {SelectGifBtn} from '#/view/com/composer/photos/SelectGifBtn' -import {SelectPhotoBtn} from '#/view/com/composer/photos/SelectPhotoBtn' import {SelectLangBtn} from '#/view/com/composer/select-language/SelectLangBtn' import {SuggestedLanguage} from '#/view/com/composer/select-language/SuggestedLanguage' // TODO: Prevent naming components that coincide with RN primitives @@ -113,12 +117,10 @@ import { type TextInputRef, } from '#/view/com/composer/text-input/TextInput' import {ThreadgateBtn} from '#/view/com/composer/threadgate/ThreadgateBtn' -import {SelectVideoBtn} from '#/view/com/composer/videos/SelectVideoBtn' import {SubtitleDialogBtn} from '#/view/com/composer/videos/SubtitleDialog' import {VideoPreview} from '#/view/com/composer/videos/VideoPreview' import {VideoTranscodeProgress} from '#/view/com/composer/videos/VideoTranscodeProgress' import {Text} from '#/view/com/util/text/Text' -import * as Toast from '#/view/com/util/Toast' import {UserAvatar} from '#/view/com/util/UserAvatar' import {atoms as a, native, useTheme, web} from '#/alf' import {Button, ButtonIcon, ButtonText} from '#/components/Button' @@ -127,8 +129,14 @@ import {EmojiArc_Stroke2_Corner0_Rounded as EmojiSmile} from '#/components/icons import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times' import {LazyQuoteEmbed} from '#/components/Post/Embed/LazyQuoteEmbed' import * as Prompt from '#/components/Prompt' +import * as toast from '#/components/Toast' import {Text as NewText} from '#/components/Typography' import {BottomSheetPortalProvider} from '../../../../modules/bottom-sheet' +import { + type AssetType, + SelectMediaButton, + type SelectMediaButtonProps, +} from './SelectMediaButton' import { type ComposerAction, composerReducer, @@ -514,12 +522,13 @@ export const ComposePost = ({ onPostSuccess?.(postSuccessData) } onClose() - Toast.show( + toast.show( thread.posts.length > 1 ? _(msg`Your posts have been published`) : replyTo ? _(msg`Your reply has been published`) : _(msg`Your post has been published`), + {type: 'success'}, ) }, [ _, @@ -654,84 +663,88 @@ export const ComposePost = ({ const isWebFooterSticky = !isNative && thread.posts.length > 1 return ( - - - 1} - publishingStage={publishingStage} - topBarAnimatedStyle={topBarAnimatedStyle} - onCancel={onPressCancel} - onPublish={onPressPublish}> - {missingAltError && } - setError('')} - clearVideo={ - erroredVideoPostId - ? () => clearVideo(erroredVideoPostId) - : () => {} - } - /> - - - - {replyTo ? : undefined} - {thread.posts.map((post, index) => ( - - 1} - isReply={index > 0 || !!replyTo} - isActive={post.id === activePost.id} - canRemovePost={thread.posts.length > 1} - canRemoveQuote={index > 0 || !initQuote} - onSelectVideo={selectVideo} - onClearVideo={clearVideo} - onPublish={onComposerPostPublish} - onError={setError} + + + + + 1} + publishingStage={publishingStage} + topBarAnimatedStyle={topBarAnimatedStyle} + onCancel={onPressCancel} + onPublish={onPressPublish}> + {missingAltError && } + setError('')} + clearVideo={ + erroredVideoPostId + ? () => clearVideo(erroredVideoPostId) + : () => {} + } /> - {isWebFooterSticky && post.id === activePost.id && ( - {footer} - )} - - ))} - - {!isWebFooterSticky && footer} - + - - + + {replyTo ? : undefined} + {thread.posts.map((post, index) => ( + + 1} + isReply={index > 0 || !!replyTo} + isActive={post.id === activePost.id} + canRemovePost={thread.posts.length > 1} + canRemoveQuote={index > 0 || !initQuote} + onSelectVideo={selectVideo} + onClearVideo={clearVideo} + onPublish={onComposerPostPublish} + onError={setError} + /> + {isWebFooterSticky && post.id === activePost.id && ( + {footer} + )} + + ))} + + {!isWebFooterSticky && footer} + + + + + + ) } @@ -811,11 +824,16 @@ let ComposerPost = React.memo(function ComposerPost({ const onPhotoPasted = useCallback( async (uri: string) => { - if (uri.startsWith('data:video/') || uri.startsWith('data:image/gif')) { + if ( + uri.startsWith('data:video/') || + (isWeb && uri.startsWith('data:image/gif')) + ) { if (isNative) return // web only const [mimeType] = uri.slice('data:'.length).split(';') if (!SUPPORTED_MIME_TYPES.includes(mimeType as SupportedMimeTypes)) { - Toast.show(_(msg`Unsupported video type`), 'xmark') + toast.show(_(msg`Unsupported video type: ${mimeType}`), { + type: 'error', + }) return } const name = `pasted.${mimeToExt(mimeType)}` @@ -1251,7 +1269,6 @@ function ComposerFooter({ dispatch, showAddButton, onEmojiButtonPress, - onError, onSelectVideo, onAddPost, }: { @@ -1266,11 +1283,32 @@ function ComposerFooter({ const t = useTheme() const {_} = useLingui() const {isMobile} = useWebMediaQueries() + /* + * Once we've allowed a certain type of asset to be selected, we don't allow + * other types of media to be selected. + */ + const [selectedAssetsType, setSelectedAssetsType] = useState< + AssetType | undefined + >(undefined) const media = post.embed.media const images = media?.type === 'images' ? media.images : [] const video = media?.type === 'video' ? media.video : null const isMaxImages = images.length >= MAX_IMAGES + const isMaxVideos = !!video + + let selectedAssetsCount = 0 + let isMediaSelectionDisabled = false + + if (media?.type === 'images') { + isMediaSelectionDisabled = isMaxImages + selectedAssetsCount = images.length + } else if (media?.type === 'video') { + isMediaSelectionDisabled = isMaxVideos + selectedAssetsCount = 1 + } else { + isMediaSelectionDisabled = !!media + } const onImageAdd = useCallback( (next: ComposerImage[]) => { @@ -1289,6 +1327,54 @@ function ComposerFooter({ [dispatch], ) + /* + * Reset if the user clears any selected media + */ + if (selectedAssetsType !== undefined && !media) { + setSelectedAssetsType(undefined) + } + + const onSelectAssets = useCallback( + async ({type, assets, errors}) => { + setSelectedAssetsType(type) + + if (assets.length) { + if (type === 'image') { + const images: ComposerImage[] = [] + + await Promise.all( + assets.map(async image => { + const composerImage = await createComposerImage({ + path: image.uri, + width: image.width, + height: image.height, + mime: image.mimeType!, + }) + images.push(composerImage) + }), + ).catch(e => { + logger.error(`createComposerImage failed`, { + safeMessage: e.message, + }) + }) + + onImageAdd(images) + } else if (type === 'video') { + onSelectVideo(post.id, assets[0]) + } else if (type === 'gif') { + onSelectVideo(post.id, assets[0]) + } + } + + errors.map(error => { + toast.show(error, { + type: 'warning', + }) + }) + }, + [post.id, onSelectVideo, onImageAdd], + ) + return ( ) : ( - - onSelectVideo(post.id, asset)} - disabled={!!media} - setError={onError} + void +} + +/** + * Generic asset classes, or buckets, that we support. + */ +export type AssetType = 'video' | 'image' | 'gif' + +/** + * Shadows `ImagePickerAsset` from `expo-image-picker`, but with a guaranteed `mimeType` + */ +type ValidatedImagePickerAsset = Omit & { + mimeType: string +} + +/** + * Codes for known validation states + */ +enum SelectedAssetError { + Unsupported = 'Unsupported', + MixedTypes = 'MixedTypes', + MaxImages = 'MaxImages', + MaxVideos = 'MaxVideos', + VideoTooLong = 'VideoTooLong', + FileTooBig = 'FileTooBig', + MaxGIFs = 'MaxGIFs', +} + +/** + * Supported video mime types. This differs slightly from + * `SUPPORTED_MIME_TYPES` from `#/lib/constants` because we only care about + * videos here. + */ +const SUPPORTED_VIDEO_MIME_TYPES = [ + 'video/mp4', + 'video/mpeg', + 'video/webm', + 'video/quicktime', +] as const +type SupportedVideoMimeType = (typeof SUPPORTED_VIDEO_MIME_TYPES)[number] +function isSupportedVideoMimeType( + mimeType: string, +): mimeType is SupportedVideoMimeType { + return SUPPORTED_VIDEO_MIME_TYPES.includes(mimeType as SupportedVideoMimeType) +} + +/** + * Supported image mime types. + */ +const SUPPORTED_IMAGE_MIME_TYPES = ( + [ + 'image/gif', + 'image/jpeg', + 'image/png', + 'image/svg+xml', + 'image/webp', + 'image/avif', + isNative && 'image/heic', + ] as const +).filter(Boolean) +type SupportedImageMimeType = Exclude< + (typeof SUPPORTED_IMAGE_MIME_TYPES)[number], + boolean +> +function isSupportedImageMimeType( + mimeType: string, +): mimeType is SupportedImageMimeType { + return SUPPORTED_IMAGE_MIME_TYPES.includes(mimeType as SupportedImageMimeType) +} + +/** + * This is a last-ditch effort type thing here, try not to rely on this. + */ +const extensionToMimeType: Record< + string, + SupportedVideoMimeType | SupportedImageMimeType +> = { + mp4: 'video/mp4', + mov: 'video/quicktime', + webm: 'video/webm', + webp: 'image/webp', + gif: 'image/gif', + jpg: 'image/jpeg', + jpeg: 'image/jpeg', + png: 'image/png', + svg: 'image/svg+xml', + heic: 'image/heic', +} + +/** + * Attempts to bucket the given asset into one of our known types based on its + * `mimeType`. If `mimeType` is not available, we try to infer it through + * various means. + */ +function classifyImagePickerAsset(asset: ImagePickerAsset): + | { + success: true + type: AssetType + mimeType: string + } + | { + success: false + type: undefined + mimeType: undefined + } { + /* + * Try to use the `mimeType` reported by `expo-image-picker` first. + */ + let mimeType = asset.mimeType + + if (!mimeType) { + /* + * We can try to infer this from the data-uri. + */ + const maybeMimeType = extractDataUriMime(asset.uri) + + if ( + maybeMimeType.startsWith('image/') || + maybeMimeType.startsWith('video/') + ) { + mimeType = maybeMimeType + } else if (maybeMimeType.startsWith('file/')) { + /* + * On the off-chance we get a `file/*` mime, try to infer from the + * extension. + */ + const extension = asset.uri.split('.').pop()?.toLowerCase() + mimeType = extensionToMimeType[extension || ''] + } + } + + if (!mimeType) { + return { + success: false, + type: undefined, + mimeType: undefined, + } + } + + /* + * Distill this down into a type "class". + */ + let type: AssetType | undefined + if (mimeType === 'image/gif') { + type = 'gif' + } else if (mimeType?.startsWith('video/')) { + type = 'video' + } else if (mimeType?.startsWith('image/')) { + type = 'image' + } + + /* + * If we weren't able to find a valid type, we don't support this asset. + */ + if (!type) { + return { + success: false, + type: undefined, + mimeType: undefined, + } + } + + return { + success: true, + type, + mimeType, + } +} + +/** + * Takes in raw assets from `expo-image-picker` and applies validation. Returns + * the dominant `AssetType`, any valid assets, and any errors encountered along + * the way. + */ +async function processImagePickerAssets( + assets: ImagePickerAsset[], + { + selectionCountRemaining, + allowedAssetTypes, + }: { + selectionCountRemaining: number + allowedAssetTypes: AssetType | undefined + }, +) { + /* + * A deduped set of error codes, which we'll use later + */ + const errors = new Set() + + /* + * We only support selecting a single type of media at a time, so this gets + * set to whatever the first valid asset type is, OR to whatever + * `allowedAssetTypes` is set to. + */ + let selectableAssetType: AssetType | undefined + + /* + * This will hold the assets that we can actually use, after filtering + */ + let supportedAssets: ValidatedImagePickerAsset[] = [] + + for (const asset of assets) { + const {success, type, mimeType} = classifyImagePickerAsset(asset) + + if (!success) { + errors.add(SelectedAssetError.Unsupported) + continue + } + + /* + * If we have an `allowedAssetTypes` prop, constrain to that. Otherwise, + * set this to the first valid asset type we see, and then use that to + * constrain all remaining selected assets. + */ + selectableAssetType = allowedAssetTypes || selectableAssetType || type + + // ignore mixed types + if (type !== selectableAssetType) { + errors.add(SelectedAssetError.MixedTypes) + continue + } + + if (type === 'video') { + /** + * We don't care too much about mimeType at this point on native, + * since the `processVideo` step later on will convert to `.mp4`. + */ + if (isWeb && !isSupportedVideoMimeType(mimeType)) { + errors.add(SelectedAssetError.Unsupported) + continue + } + + /* + * Filesize appears to be stable across all platforms, so we can use it + * to filter out large files on web. On native, we compress these anyway, + * so we only check on web. + */ + if (isWeb && asset.fileSize && asset.fileSize > VIDEO_MAX_SIZE) { + errors.add(SelectedAssetError.FileTooBig) + continue + } + } + + if (type === 'image') { + if (!isSupportedImageMimeType(mimeType)) { + errors.add(SelectedAssetError.Unsupported) + continue + } + } + + if (type === 'gif') { + /* + * Filesize appears to be stable across all platforms, so we can use it + * to filter out large files on web. On native, we compress GIFs as + * videos anyway, so we only check on web. + */ + if (isWeb && asset.fileSize && asset.fileSize > VIDEO_MAX_SIZE) { + errors.add(SelectedAssetError.FileTooBig) + continue + } + } + + /* + * All validations passed, we have an asset! + */ + supportedAssets.push({ + mimeType, + ...asset, + /* + * In `expo-image-picker` >= v17, `uri` is now a `blob:` URL, not a + * data-uri. Our handling elsewhere in the app (for web) relies on the + * base64 data-uri, so we construct it here for web only. + */ + uri: + isWeb && asset.base64 + ? `data:${mimeType};base64,${asset.base64}` + : asset.uri, + }) + } + + if (supportedAssets.length > 0) { + if (selectableAssetType === 'image') { + if (supportedAssets.length > selectionCountRemaining) { + errors.add(SelectedAssetError.MaxImages) + supportedAssets = supportedAssets.slice(0, selectionCountRemaining) + } + } else if (selectableAssetType === 'video') { + if (supportedAssets.length > 1) { + errors.add(SelectedAssetError.MaxVideos) + supportedAssets = supportedAssets.slice(0, 1) + } + + if (supportedAssets[0].duration) { + if (isWeb) { + /* + * Web reports duration as seconds + */ + supportedAssets[0].duration = supportedAssets[0].duration * 1000 + } + + if (supportedAssets[0].duration > VIDEO_MAX_DURATION_MS) { + errors.add(SelectedAssetError.VideoTooLong) + supportedAssets = [] + } + } else { + errors.add(SelectedAssetError.Unsupported) + supportedAssets = [] + } + } else if (selectableAssetType === 'gif') { + if (supportedAssets.length > 1) { + errors.add(SelectedAssetError.MaxGIFs) + supportedAssets = supportedAssets.slice(0, 1) + } + } + } + + return { + type: selectableAssetType!, // set above + assets: supportedAssets, + errors, + } +} + +export function SelectMediaButton({ + disabled, + allowedAssetTypes, + selectedAssetsCount, + onSelectAssets, +}: SelectMediaButtonProps) { + const {_} = useLingui() + const {requestPhotoAccessIfNeeded} = usePhotoLibraryPermission() + const {requestVideoAccessIfNeeded} = useVideoLibraryPermission() + const sheetWrapper = useSheetWrapper() + const t = useTheme() + + const selectionCountRemaining = MAX_IMAGES - selectedAssetsCount + + const processSelectedAssets = useCallback( + async (rawAssets: ImagePickerAsset[]) => { + const { + type, + assets, + errors: errorCodes, + } = await processImagePickerAssets(rawAssets, { + selectionCountRemaining, + allowedAssetTypes, + }) + + /* + * Convert error codes to user-friendly messages. + */ + const errors = Array.from(errorCodes).map(error => { + return { + [SelectedAssetError.Unsupported]: _( + msg`One or more of your selected files are not supported.`, + ), + [SelectedAssetError.MixedTypes]: _( + msg`Selecting multiple media types is not supported.`, + ), + [SelectedAssetError.MaxImages]: _( + msg({ + message: `You can select up to ${plural(MAX_IMAGES, { + other: '# images', + })} in total.`, + comment: `Error message for maximum number of images that can be selected to add to a post, currently 4 but may change.`, + }), + ), + [SelectedAssetError.MaxVideos]: _( + msg`You can only select one video at a time.`, + ), + [SelectedAssetError.VideoTooLong]: _( + msg`Videos must be less than 3 minutes long.`, + ), + [SelectedAssetError.MaxGIFs]: _( + msg`You can only select one GIF at a time.`, + ), + [SelectedAssetError.FileTooBig]: _( + msg`One or more of your selected files is too large. Maximum size is 100 MB.`, + ), + }[error] + }) + + /* + * Report the selected assets and any errors back to the + * composer. + */ + onSelectAssets({ + type, + assets, + errors, + }) + }, + [_, onSelectAssets, selectionCountRemaining, allowedAssetTypes], + ) + + const onPressSelectMedia = useCallback(async () => { + if (isNative) { + const [photoAccess, videoAccess] = await Promise.all([ + requestPhotoAccessIfNeeded(), + requestVideoAccessIfNeeded(), + ]) + + if (!photoAccess && !videoAccess) { + toast.show(_(msg`You need to allow access to your media library.`), { + type: 'error', + }) + return + } + } + + if (isNative && Keyboard.isVisible()) { + Keyboard.dismiss() + } + + const {assets, canceled} = await sheetWrapper( + launchImageLibraryAsync({ + exif: false, + mediaTypes: ['images', 'videos'], + quality: 1, + allowsMultipleSelection: true, + legacy: true, + base64: isWeb, + selectionLimit: isIOS ? selectionCountRemaining : undefined, + preferredAssetRepresentationMode: + UIImagePickerPreferredAssetRepresentationMode.Current, + videoMaxDuration: VIDEO_MAX_DURATION_MS / 1000, + }), + ) + + if (canceled) return + + await processSelectedAssets(assets) + }, [ + _, + requestPhotoAccessIfNeeded, + requestVideoAccessIfNeeded, + sheetWrapper, + processSelectedAssets, + selectionCountRemaining, + ]) + + return ( + + ) +} diff --git a/src/view/com/composer/photos/ImageAltTextDialog.tsx b/src/view/com/composer/photos/ImageAltTextDialog.tsx index 724149937c..b356cde9b7 100644 --- a/src/view/com/composer/photos/ImageAltTextDialog.tsx +++ b/src/view/com/composer/photos/ImageAltTextDialog.tsx @@ -96,13 +96,12 @@ const ImageAltTextInner = ({ diff --git a/src/view/com/composer/photos/SelectPhotoBtn.tsx b/src/view/com/composer/photos/SelectPhotoBtn.tsx deleted file mode 100644 index f4c6aa328a..0000000000 --- a/src/view/com/composer/photos/SelectPhotoBtn.tsx +++ /dev/null @@ -1,60 +0,0 @@ -/* eslint-disable react-native-a11y/has-valid-accessibility-ignores-invert-colors */ -import {useCallback} from 'react' -import {msg} from '@lingui/macro' -import {useLingui} from '@lingui/react' - -import {usePhotoLibraryPermission} from '#/lib/hooks/usePermissions' -import {openPicker} from '#/lib/media/picker' -import {isNative} from '#/platform/detection' -import {ComposerImage, createComposerImage} from '#/state/gallery' -import {atoms as a, useTheme} from '#/alf' -import {Button} from '#/components/Button' -import {useSheetWrapper} from '#/components/Dialog/sheet-wrapper' -import {Image_Stroke2_Corner0_Rounded as Image} from '#/components/icons/Image' - -type Props = { - size: number - disabled?: boolean - onAdd: (next: ComposerImage[]) => void -} - -export function SelectPhotoBtn({size, disabled, onAdd}: Props) { - const {_} = useLingui() - const {requestPhotoAccessIfNeeded} = usePhotoLibraryPermission() - const t = useTheme() - const sheetWrapper = useSheetWrapper() - - const onPressSelectPhotos = useCallback(async () => { - if (isNative && !(await requestPhotoAccessIfNeeded())) { - return - } - - const images = await sheetWrapper( - openPicker({ - selectionLimit: 4 - size, - allowsMultipleSelection: true, - }), - ) - - const results = await Promise.all( - images.map(img => createComposerImage(img)), - ) - - onAdd(results) - }, [requestPhotoAccessIfNeeded, size, onAdd, sheetWrapper]) - - return ( - - ) -} diff --git a/src/view/com/composer/videos/SelectVideoBtn.tsx b/src/view/com/composer/videos/SelectVideoBtn.tsx deleted file mode 100644 index 96715955f0..0000000000 --- a/src/view/com/composer/videos/SelectVideoBtn.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import {useCallback} from 'react' -import {type ImagePickerAsset} from 'expo-image-picker' -import {msg} from '@lingui/macro' -import {useLingui} from '@lingui/react' - -import { - SUPPORTED_MIME_TYPES, - type SupportedMimeTypes, - VIDEO_MAX_DURATION_MS, -} from '#/lib/constants' -import {useVideoLibraryPermission} from '#/lib/hooks/usePermissions' -import {isWeb} from '#/platform/detection' -import {isNative} from '#/platform/detection' -import {atoms as a, useTheme} from '#/alf' -import {Button} from '#/components/Button' -import {VideoClip_Stroke2_Corner0_Rounded as VideoClipIcon} from '#/components/icons/VideoClip' -import {pickVideo} from './pickVideo' - -type Props = { - onSelectVideo: (video: ImagePickerAsset) => void - disabled?: boolean - setError: (error: string) => void -} - -export function SelectVideoBtn({onSelectVideo, disabled, setError}: Props) { - const {_} = useLingui() - const t = useTheme() - const {requestVideoAccessIfNeeded} = useVideoLibraryPermission() - - const onPressSelectVideo = useCallback(async () => { - if (isNative && !(await requestVideoAccessIfNeeded())) { - return - } - - const response = await pickVideo() - if (response.assets && response.assets.length > 0) { - const asset = response.assets[0] - try { - if (isWeb) { - // asset.duration is null for gifs (see the TODO in pickVideo.web.ts) - if (asset.duration && asset.duration > VIDEO_MAX_DURATION_MS) { - throw Error(_(msg`Videos must be less than 3 minutes long`)) - } - // compression step on native converts to mp4, so no need to check there - if ( - !SUPPORTED_MIME_TYPES.includes(asset.mimeType as SupportedMimeTypes) - ) { - throw Error(_(msg`Unsupported video type: ${asset.mimeType}`)) - } - } else { - if (typeof asset.duration !== 'number') { - throw Error('Asset is not a video') - } - if (asset.duration > VIDEO_MAX_DURATION_MS) { - throw Error(_(msg`Videos must be less than 3 minutes long`)) - } - } - onSelectVideo(asset) - } catch (err) { - if (err instanceof Error) { - setError(err.message) - } else { - setError(_(msg`An error occurred while selecting the video`)) - } - } - } - }, [requestVideoAccessIfNeeded, setError, _, onSelectVideo]) - - return ( - <> - - - ) -} diff --git a/src/view/com/composer/videos/VideoPreview.tsx b/src/view/com/composer/videos/VideoPreview.tsx index 255174beab..84cb1dba75 100644 --- a/src/view/com/composer/videos/VideoPreview.tsx +++ b/src/view/com/composer/videos/VideoPreview.tsx @@ -1,9 +1,10 @@ import React from 'react' import {View} from 'react-native' -import {ImagePickerAsset} from 'expo-image-picker' +import {Image} from 'expo-image' +import {type ImagePickerAsset} from 'expo-image-picker' import {BlueskyVideoView} from '@haileyok/bluesky-video' -import {CompressedVideo} from '#/lib/media/video/types' +import {type CompressedVideo} from '#/lib/media/video/types' import {clamp} from '#/lib/numbers' import {useAutoplayDisabled} from '#/state/preferences' import {ExternalEmbedRemoveBtn} from '#/view/com/composer/ExternalEmbedRemoveBtn' @@ -48,13 +49,25 @@ export function VideoPreview({ {isActivePost && ( - + <> + {video.mimeType === 'image/gif' ? ( + + ) : ( + + )} + )} {autoplayDisabled && ( diff --git a/yarn.lock b/yarn.lock index dc6489bf85..9145826a4a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11288,6 +11288,11 @@ expo-image-loader@~5.1.0: resolved "https://registry.yarnpkg.com/expo-image-loader/-/expo-image-loader-5.1.0.tgz#f7d65f9b9a9714eaaf5d50a406cb34cb25262153" integrity sha512-sEBx3zDQIODWbB5JwzE7ZL5FJD+DK3LVLWBVJy6VzsqIA6nDEnSFnsnWyCfCTSvbGigMATs1lgkC2nz3Jpve1Q== +expo-image-loader@~6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/expo-image-loader/-/expo-image-loader-6.0.0.tgz#15230442cbb90e101c080a4c81e37d974e43e072" + integrity sha512-nKs/xnOGw6ACb4g26xceBD57FKLFkSwEUTDXEDF3Gtcu3MqF3ZIYd3YM+sSb1/z9AKV1dYT7rMSGVNgsveXLIQ== + expo-image-manipulator@~13.1.7: version "13.1.7" resolved "https://registry.yarnpkg.com/expo-image-manipulator/-/expo-image-manipulator-13.1.7.tgz#e891ce9b49d75962eafdf5b7d670116583379e76" @@ -11295,12 +11300,12 @@ expo-image-manipulator@~13.1.7: dependencies: expo-image-loader "~5.1.0" -expo-image-picker@~16.1.4: - version "16.1.4" - resolved "https://registry.yarnpkg.com/expo-image-picker/-/expo-image-picker-16.1.4.tgz#d4ac2d1f64f6ec9347c3f64f8435b40e6e4dcc40" - integrity sha512-bTmmxtw1AohUT+HxEBn2vYwdeOrj1CLpMXKjvi9FKSoSbpcarT4xxI0z7YyGwDGHbrJqyyic3I9TTdP2J2b4YA== +expo-image-picker@^17.0.2: + version "17.0.2" + resolved "https://registry.yarnpkg.com/expo-image-picker/-/expo-image-picker-17.0.2.tgz#79af7192b2947e54686d0ece6ccbb5f6a178a809" + integrity sha512-O74FIrc37KB4ZxC/BMUL3fEZwdmIB60As0q5XczRlzPvWismBl7GG3pPy+o5SGUI2jcepTvQAa2PcNcMbUZNYg== dependencies: - expo-image-loader "~5.1.0" + expo-image-loader "~6.0.0" expo-image@^2.4.0: version "2.4.0" From 92ad887e3881a04339144346dfad24007b3b7704 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Mon, 18 Aug 2025 19:24:54 -0500 Subject: [PATCH 2/6] Add underage notice to Moderation screen (#8868) * Add underage notice to Moderation screen * Incorporate suggestion Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> --------- Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> --- src/screens/Moderation/index.tsx | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/screens/Moderation/index.tsx b/src/screens/Moderation/index.tsx index 1517792a19..e829abd473 100644 --- a/src/screens/Moderation/index.tsx +++ b/src/screens/Moderation/index.tsx @@ -22,6 +22,7 @@ import { import {isNonConfigurableModerationAuthority} from '#/state/session/additional-moderation-authorities' import {useSetMinimalShellMode} from '#/state/shell' import {atoms as a, useBreakpoints, useTheme, type ViewStyleProp} from '#/alf' +import {Admonition} from '#/components/Admonition' import {AgeAssuranceAdmonition} from '#/components/ageAssurance/AgeAssuranceAdmonition' import {Button, ButtonText} from '#/components/Button' import * as Dialog from '#/components/Dialog' @@ -201,6 +202,24 @@ export function ModerationScreenInner({ return ( + {isDeclaredUnderage && ( + + + + Your declared age is under 18. Some settings below may be + disabled. If this was a mistake, you may edit your bithdate in + your{' '} + + account settings + + . + + + + )} + Moderation tools From 48baf7277df4b447521d372db9ca3f7f3ca7b73b Mon Sep 17 00:00:00 2001 From: pfrazee <1270099+pfrazee@users.noreply.github.com> Date: Tue, 19 Aug 2025 02:39:11 +0000 Subject: [PATCH 3/6] Nightly source-language update --- src/locale/locales/en/messages.po | 248 ++++++++++++++++-------------- 1 file changed, 134 insertions(+), 114 deletions(-) diff --git a/src/locale/locales/en/messages.po b/src/locale/locales/en/messages.po index 9d96abb7a5..725d03495d 100644 --- a/src/locale/locales/en/messages.po +++ b/src/locale/locales/en/messages.po @@ -153,7 +153,7 @@ msgstr "" msgid "{0} joined this week" msgstr "" -#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/Scrubber.tsx:202 +#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/Scrubber.tsx:204 msgid "{0} of {1}" msgstr "" @@ -687,7 +687,7 @@ msgstr "" msgid "Add another account" msgstr "" -#: src/view/com/composer/Composer.tsx:780 +#: src/view/com/composer/Composer.tsx:793 msgid "Add another post" msgstr "" @@ -705,6 +705,11 @@ msgstr "" msgid "Add emoji reaction" msgstr "" +#. Accessibility label for button in composer to add photos or a video to a post +#: src/view/com/composer/SelectMediaButton.tsx:482 +msgid "Add media to post" +msgstr "" + #: src/components/moderation/ReportDialog/index.tsx:403 #: src/components/moderation/ReportDialog/index.tsx:407 msgid "Add more details (optional)" @@ -718,7 +723,7 @@ msgstr "" msgid "Add muted words and tags" msgstr "" -#: src/view/com/composer/Composer.tsx:1344 +#: src/view/com/composer/Composer.tsx:1426 msgid "Add new post" msgstr "" @@ -788,7 +793,7 @@ msgstr "" msgid "Adult Content" msgstr "" -#: src/screens/Moderation/index.tsx:404 +#: src/screens/Moderation/index.tsx:423 msgid "Adult content can only be enabled via the Web at <0>bsky.app." msgstr "" @@ -801,7 +806,7 @@ msgstr "" msgid "Adult Content labels" msgstr "" -#: src/screens/Moderation/index.tsx:454 +#: src/screens/Moderation/index.tsx:473 msgid "Advanced" msgstr "" @@ -888,7 +893,7 @@ msgstr "" #: src/screens/Settings/AccessibilitySettings.tsx:54 #: src/view/com/composer/GifAltText.tsx:154 -#: src/view/com/composer/photos/ImageAltTextDialog.tsx:118 +#: src/view/com/composer/photos/ImageAltTextDialog.tsx:117 #: src/view/com/composer/videos/SubtitleDialog.tsx:40 #: src/view/com/composer/videos/SubtitleDialog.tsx:55 #: src/view/com/composer/videos/SubtitleDialog.tsx:101 @@ -905,7 +910,7 @@ msgid "Alt text describes images for blind and low-vision users, and helps give msgstr "" #: src/view/com/composer/GifAltText.tsx:179 -#: src/view/com/composer/photos/ImageAltTextDialog.tsx:139 +#: src/view/com/composer/photos/ImageAltTextDialog.tsx:138 msgid "Alt text will be truncated. {MAX_ALT_TEXT, plural, other {Limit: {0} characters.}}" msgstr "" @@ -917,7 +922,7 @@ msgstr "" msgid "An error has occurred" msgstr "" -#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx:420 +#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx:422 msgid "An error occurred" msgstr "" @@ -945,10 +950,6 @@ msgstr "" msgid "An error occurred while saving the QR code!" msgstr "" -#: src/view/com/composer/videos/SelectVideoBtn.tsx:63 -msgid "An error occurred while selecting the video" -msgstr "" - #: src/screens/StarterPack/StarterPackScreen.tsx:352 #: src/screens/StarterPack/StarterPackScreen.tsx:374 msgid "An error occurred while trying to follow all" @@ -1164,11 +1165,11 @@ msgstr "" msgid "Are you sure you want to remove this from your feeds?" msgstr "" -#: src/view/com/composer/Composer.tsx:729 +#: src/view/com/composer/Composer.tsx:741 msgid "Are you sure you'd like to discard this draft?" msgstr "" -#: src/view/com/composer/Composer.tsx:914 +#: src/view/com/composer/Composer.tsx:932 msgid "Are you sure you'd like to discard this post?" msgstr "" @@ -1337,7 +1338,7 @@ msgstr "" msgid "Blocked" msgstr "" -#: src/screens/Moderation/index.tsx:282 +#: src/screens/Moderation/index.tsx:301 msgid "Blocked accounts" msgstr "" @@ -1557,8 +1558,8 @@ msgstr "" #: src/screens/Settings/Settings.tsx:289 #: src/screens/Takendown.tsx:99 #: src/screens/Takendown.tsx:102 -#: src/view/com/composer/Composer.tsx:969 -#: src/view/com/composer/Composer.tsx:980 +#: src/view/com/composer/Composer.tsx:987 +#: src/view/com/composer/Composer.tsx:998 #: src/view/com/composer/photos/EditImageDialog.web.tsx:43 #: src/view/com/composer/photos/EditImageDialog.web.tsx:52 #: src/view/com/modals/ChangePassword.tsx:279 @@ -1922,7 +1923,7 @@ msgstr "" msgid "Closes password update alert" msgstr "" -#: src/view/com/composer/Composer.tsx:977 +#: src/view/com/composer/Composer.tsx:995 msgid "Closes post composer and discards post draft" msgstr "" @@ -1980,7 +1981,7 @@ msgstr "" msgid "Compose new post" msgstr "" -#: src/view/com/composer/Composer.tsx:878 +#: src/view/com/composer/Composer.tsx:896 msgid "Compose posts up to {0, plural, other {# characters}} in length" msgstr "" @@ -1988,7 +1989,7 @@ msgstr "" msgid "Compose reply" msgstr "" -#: src/view/com/composer/Composer.tsx:1738 +#: src/view/com/composer/Composer.tsx:1820 msgid "Compressing video..." msgstr "" @@ -2015,11 +2016,11 @@ msgstr "" msgid "Confirm delete account" msgstr "" -#: src/screens/Moderation/index.tsx:330 +#: src/screens/Moderation/index.tsx:349 msgid "Confirm your age:" msgstr "" -#: src/screens/Moderation/index.tsx:321 +#: src/screens/Moderation/index.tsx:340 msgid "Confirm your birthdate" msgstr "" @@ -2072,8 +2073,8 @@ msgstr "" msgid "Content Blocked" msgstr "" -#: src/screens/Moderation/index.tsx:317 -#: src/screens/Moderation/index.tsx:351 +#: src/screens/Moderation/index.tsx:336 +#: src/screens/Moderation/index.tsx:370 msgid "Content filters" msgstr "" @@ -2518,7 +2519,7 @@ msgstr "" #: src/components/PostControls/PostMenu/PostMenuItems.tsx:678 #: src/components/PostControls/PostMenu/PostMenuItems.tsx:680 -#: src/view/com/composer/Composer.tsx:888 +#: src/view/com/composer/Composer.tsx:906 msgid "Delete post" msgstr "" @@ -2566,7 +2567,7 @@ msgid "Description" msgstr "" #: src/view/com/composer/GifAltText.tsx:150 -#: src/view/com/composer/photos/ImageAltTextDialog.tsx:114 +#: src/view/com/composer/photos/ImageAltTextDialog.tsx:113 msgid "Descriptive alt text" msgstr "" @@ -2620,7 +2621,7 @@ msgstr "" msgid "Disable haptic feedback" msgstr "" -#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx:386 +#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx:388 msgid "Disable subtitles" msgstr "" @@ -2629,13 +2630,13 @@ msgstr "" #: src/lib/moderation/useLabelBehaviorDescription.ts:68 #: src/screens/Messages/Settings.tsx:155 #: src/screens/Messages/Settings.tsx:158 -#: src/screens/Moderation/index.tsx:394 +#: src/screens/Moderation/index.tsx:413 msgid "Disabled" msgstr "" #: src/screens/Profile/Header/EditProfileDialog.tsx:89 -#: src/view/com/composer/Composer.tsx:731 -#: src/view/com/composer/Composer.tsx:921 +#: src/view/com/composer/Composer.tsx:743 +#: src/view/com/composer/Composer.tsx:939 msgid "Discard" msgstr "" @@ -2643,11 +2644,11 @@ msgstr "" msgid "Discard changes?" msgstr "" -#: src/view/com/composer/Composer.tsx:728 +#: src/view/com/composer/Composer.tsx:740 msgid "Discard draft?" msgstr "" -#: src/view/com/composer/Composer.tsx:913 +#: src/view/com/composer/Composer.tsx:931 msgid "Discard post?" msgstr "" @@ -2673,7 +2674,7 @@ msgstr "" msgid "Dismiss" msgstr "" -#: src/view/com/composer/Composer.tsx:1662 +#: src/view/com/composer/Composer.tsx:1744 msgid "Dismiss error" msgstr "" @@ -3002,7 +3003,7 @@ msgstr "" msgid "Enable {0} only" msgstr "" -#: src/screens/Moderation/index.tsx:381 +#: src/screens/Moderation/index.tsx:400 msgid "Enable adult content" msgstr "" @@ -3028,7 +3029,7 @@ msgstr "" msgid "Enable push notifications" msgstr "" -#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx:387 +#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx:389 msgid "Enable subtitles" msgstr "" @@ -3048,7 +3049,7 @@ msgstr "" #: src/screens/Messages/Settings.tsx:146 #: src/screens/Messages/Settings.tsx:149 -#: src/screens/Moderation/index.tsx:392 +#: src/screens/Moderation/index.tsx:411 msgid "Enabled" msgstr "" @@ -3074,7 +3075,7 @@ msgstr "" msgid "Enter code" msgstr "" -#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx:405 +#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx:407 msgid "Enter fullscreen" msgstr "" @@ -3119,7 +3120,7 @@ msgstr "" msgid "Entertainment" msgstr "" -#: src/view/com/composer/Composer.tsx:1747 +#: src/view/com/composer/Composer.tsx:1829 #: src/view/com/util/error/ErrorScreen.tsx:42 msgid "Error" msgstr "" @@ -3194,7 +3195,7 @@ msgstr "" msgid "Excludes users you follow" msgstr "" -#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx:404 +#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx:406 msgid "Exit fullscreen" msgstr "" @@ -3810,10 +3811,6 @@ msgctxt "from-feed" msgid "From <0/>" msgstr "" -#: src/view/com/composer/photos/SelectPhotoBtn.tsx:50 -msgid "Gallery" -msgstr "" - #: src/components/StarterPack/ProfileStarterPacks.tsx:307 msgid "Generate a starter pack" msgstr "" @@ -3967,6 +3964,7 @@ msgstr "" #: src/components/ageAssurance/AgeAssuranceAdmonition.tsx:89 #: src/components/ageAssurance/AgeRestrictedScreen.tsx:75 #: src/components/ageAssurance/AgeRestrictedScreen.tsx:84 +#: src/screens/Moderation/index.tsx:214 msgid "Go to account settings" msgstr "" @@ -4176,7 +4174,7 @@ msgstr "" msgid "Hmm, we're having trouble finding this feed. It may have been deleted." msgstr "" -#: src/screens/Moderation/index.tsx:59 +#: src/screens/Moderation/index.tsx:60 msgid "Hmmmm, it seems we're having trouble loading this data. See below for more details. If this issue persists, please contact us." msgstr "" @@ -4382,7 +4380,7 @@ msgstr "" msgid "Interaction limited" msgstr "" -#: src/screens/Moderation/index.tsx:222 +#: src/screens/Moderation/index.tsx:241 msgid "Interaction settings" msgstr "" @@ -4451,7 +4449,7 @@ msgstr "" msgid "It's just you right now! Add more people to your starter pack by searching above." msgstr "" -#: src/view/com/composer/Composer.tsx:1681 +#: src/view/com/composer/Composer.tsx:1763 msgid "Job ID: {0}" msgstr "" @@ -4927,7 +4925,7 @@ msgstr "" msgid "Manage saved feeds" msgstr "" -#: src/screens/Moderation/index.tsx:292 +#: src/screens/Moderation/index.tsx:311 msgid "Manage verification settings" msgstr "" @@ -5045,7 +5043,7 @@ msgid "Misleading Post" msgstr "" #: src/Navigation.tsx:176 -#: src/screens/Moderation/index.tsx:99 +#: src/screens/Moderation/index.tsx:100 #: src/screens/Settings/Settings.tsx:188 #: src/screens/Settings/Settings.tsx:191 msgid "Moderation" @@ -5079,7 +5077,7 @@ msgctxt "toast" msgid "Moderation list updated" msgstr "" -#: src/screens/Moderation/index.tsx:252 +#: src/screens/Moderation/index.tsx:271 msgid "Moderation lists" msgstr "" @@ -5096,7 +5094,7 @@ msgstr "" msgid "Moderation states" msgstr "" -#: src/screens/Moderation/index.tsx:206 +#: src/screens/Moderation/index.tsx:225 msgid "Moderation tools" msgstr "" @@ -5211,7 +5209,7 @@ msgstr "" msgid "Mute words & tags" msgstr "" -#: src/screens/Moderation/index.tsx:267 +#: src/screens/Moderation/index.tsx:286 msgid "Muted accounts" msgstr "" @@ -5228,7 +5226,7 @@ msgstr "" msgid "Muted by \"{0}\"" msgstr "" -#: src/screens/Moderation/index.tsx:237 +#: src/screens/Moderation/index.tsx:256 msgid "Muted words & tags" msgstr "" @@ -5722,15 +5720,23 @@ msgstr "" msgid "Onboarding reset" msgstr "" -#: src/view/com/composer/Composer.tsx:347 +#: src/view/com/composer/Composer.tsx:355 msgid "One or more GIFs is missing alt text." msgstr "" -#: src/view/com/composer/Composer.tsx:344 +#: src/view/com/composer/Composer.tsx:352 msgid "One or more images is missing alt text." msgstr "" -#: src/view/com/composer/Composer.tsx:354 +#: src/view/com/composer/SelectMediaButton.tsx:390 +msgid "One or more of your selected files are not supported." +msgstr "" + +#: src/view/com/composer/SelectMediaButton.tsx:413 +msgid "One or more of your selected files is too large. Maximum size is 100 MB." +msgstr "" + +#: src/view/com/composer/Composer.tsx:362 msgid "One or more videos is missing alt text." msgstr "" @@ -5748,7 +5754,7 @@ msgstr "" msgid "Only followers who I follow" msgstr "" -#: src/lib/media/picker.shared.ts:32 +#: src/lib/media/picker.shared.ts:28 msgid "Only image files are supported" msgstr "" @@ -5787,7 +5793,7 @@ msgid "Open drawer menu" msgstr "" #: src/screens/Messages/components/MessageInput.web.tsx:181 -#: src/view/com/composer/Composer.tsx:1329 +#: src/view/com/composer/Composer.tsx:1411 msgid "Open emoji picker" msgstr "" @@ -5816,7 +5822,7 @@ msgstr "" msgid "Open moderation debug page" msgstr "" -#: src/screens/Moderation/index.tsx:233 +#: src/screens/Moderation/index.tsx:252 msgid "Open muted words and tags settings" msgstr "" @@ -5886,11 +5892,17 @@ msgstr "" msgid "Opens composer" msgstr "" -#: src/view/com/composer/photos/SelectPhotoBtn.tsx:51 -msgid "Opens device photo gallery" +#. Accessibility hint on web for button in composer to add images, a video, or a GIF to a post. Maximum number of images that can be selected is currently 4 but may change. +#: src/view/com/composer/SelectMediaButton.tsx:501 +msgid "Opens device gallery to select up to {MAX_IMAGES, plural, other {# images}}, or a single video or GIF." msgstr "" -#: src/view/com/composer/Composer.tsx:1330 +#. Accessibility hint on native for button in composer to add images or a video to a post. Maximum number of images that can be selected is currently 4 but may change. +#: src/view/com/composer/SelectMediaButton.tsx:490 +msgid "Opens device gallery to select up to {MAX_IMAGES, plural, other {# images}}, or a single video." +msgstr "" + +#: src/view/com/composer/Composer.tsx:1412 msgid "Opens emoji picker" msgstr "" @@ -5933,10 +5945,6 @@ msgstr "" msgid "Opens this profile" msgstr "" -#: src/view/com/composer/videos/SelectVideoBtn.tsx:75 -msgid "Opens video picker" -msgstr "" - #: src/components/dms/ReportDialog.tsx:221 #: src/components/ReportDialog/SubmitView.tsx:168 msgid "Optionally provide additional information below:" @@ -6310,12 +6318,12 @@ msgctxt "description" msgid "Post" msgstr "" -#: src/view/com/composer/Composer.tsx:1040 +#: src/view/com/composer/Composer.tsx:1058 msgctxt "action" msgid "Post" msgstr "" -#: src/view/com/composer/Composer.tsx:1038 +#: src/view/com/composer/Composer.tsx:1056 msgctxt "action" msgid "Post All" msgstr "" @@ -6490,7 +6498,7 @@ msgstr "" msgid "Privacy Policy" msgstr "" -#: src/view/com/composer/Composer.tsx:1744 +#: src/view/com/composer/Composer.tsx:1826 msgid "Processing video..." msgstr "" @@ -6529,22 +6537,22 @@ msgid "Public, sharable lists which can be used to drive feeds." msgstr "" #. Accessibility label for button to publish a single post -#: src/view/com/composer/Composer.tsx:1020 +#: src/view/com/composer/Composer.tsx:1038 msgid "Publish post" msgstr "" #. Accessibility label for button to publish multiple posts in a thread -#: src/view/com/composer/Composer.tsx:1013 +#: src/view/com/composer/Composer.tsx:1031 msgid "Publish posts" msgstr "" #. Accessibility label for button to publish multiple replies in a thread -#: src/view/com/composer/Composer.tsx:998 +#: src/view/com/composer/Composer.tsx:1016 msgid "Publish replies" msgstr "" #. Accessibility label for button to publish a single reply -#: src/view/com/composer/Composer.tsx:1005 +#: src/view/com/composer/Composer.tsx:1023 msgid "Publish reply" msgstr "" @@ -6911,7 +6919,7 @@ msgstr "" msgid "Replies to this post are disabled." msgstr "" -#: src/view/com/composer/Composer.tsx:1036 +#: src/view/com/composer/Composer.tsx:1054 msgctxt "action" msgid "Reply" msgstr "" @@ -7239,8 +7247,8 @@ msgstr "" #: src/view/com/composer/GifAltText.tsx:202 #: src/view/com/composer/photos/EditImageDialog.web.tsx:62 #: src/view/com/composer/photos/EditImageDialog.web.tsx:75 -#: src/view/com/composer/photos/ImageAltTextDialog.tsx:153 -#: src/view/com/composer/photos/ImageAltTextDialog.tsx:163 +#: src/view/com/composer/photos/ImageAltTextDialog.tsx:152 +#: src/view/com/composer/photos/ImageAltTextDialog.tsx:162 #: src/view/com/modals/CreateOrEditList.tsx:315 #: src/view/screens/SavedFeeds.tsx:117 msgid "Save" @@ -7440,7 +7448,7 @@ msgstr "" msgid "See this guide" msgstr "" -#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/Scrubber.tsx:195 +#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/Scrubber.tsx:197 msgid "Seek slider. Use the arrow keys to seek forwards and backwards, and space to play/pause" msgstr "" @@ -7539,10 +7547,6 @@ msgstr "" msgid "Select the moderation service(s) to report to" msgstr "" -#: src/view/com/composer/videos/SelectVideoBtn.tsx:74 -msgid "Select video" -msgstr "" - #: src/components/dialogs/MutedWords.tsx:242 msgid "Select what content this mute word should apply to." msgstr "" @@ -7572,6 +7576,10 @@ msgstr "" msgid "Select your preferred notification channels" msgstr "" +#: src/view/com/composer/SelectMediaButton.tsx:393 +msgid "Selecting multiple media types is not supported." +msgstr "" + #: src/view/com/util/forms/DropdownButton.tsx:302 msgid "Selects option {0} of {numItems}" msgstr "" @@ -7649,7 +7657,7 @@ msgstr "" msgid "Set app icon to {0}" msgstr "" -#: src/screens/Moderation/index.tsx:333 +#: src/screens/Moderation/index.tsx:352 msgid "Set birthdate" msgstr "" @@ -8077,7 +8085,7 @@ msgid "Something went wrong, please try again" msgstr "" #: src/components/ReportDialog/index.tsx:54 -#: src/screens/Moderation/index.tsx:111 +#: src/screens/Moderation/index.tsx:112 #: src/screens/Profile/Sections/Labels.tsx:184 msgid "Something went wrong, please try again." msgstr "" @@ -8482,7 +8490,7 @@ msgstr "" msgid "The author of this thread has hidden this reply." msgstr "" -#: src/screens/Moderation/index.tsx:407 +#: src/screens/Moderation/index.tsx:426 msgid "The Bluesky web application" msgstr "" @@ -8840,7 +8848,7 @@ msgstr "" msgid "This post will be hidden from feeds and threads. This cannot be undone." msgstr "" -#: src/view/com/composer/Composer.tsx:463 +#: src/view/com/composer/Composer.tsx:471 msgid "This post's author has disabled quote posts." msgstr "" @@ -8974,7 +8982,7 @@ msgstr "" msgid "Toggle dropdown" msgstr "" -#: src/screens/Moderation/index.tsx:384 +#: src/screens/Moderation/index.tsx:403 msgid "Toggle to enable or disable adult content" msgstr "" @@ -9256,12 +9264,8 @@ msgstr "" msgid "Unsubscribed from list" msgstr "" -#: src/view/com/composer/Composer.tsx:818 -msgid "Unsupported video type" -msgstr "" - -#: src/view/com/composer/videos/SelectVideoBtn.tsx:48 -msgid "Unsupported video type: {0}" +#: src/view/com/composer/Composer.tsx:834 +msgid "Unsupported video type: {mimeType}" msgstr "" #: src/components/moderation/ReportDialog/utils/useReportOptions.ts:77 @@ -9346,7 +9350,7 @@ msgstr "" msgid "Uploading link thumbnail..." msgstr "" -#: src/view/com/composer/Composer.tsx:1741 +#: src/view/com/composer/Composer.tsx:1823 msgid "Uploading video..." msgstr "" @@ -9482,7 +9486,7 @@ msgstr "" msgid "Verification failed, please try again." msgstr "" -#: src/screens/Moderation/index.tsx:297 +#: src/screens/Moderation/index.tsx:316 msgid "Verification settings" msgstr "" @@ -9608,7 +9612,7 @@ msgstr "" msgid "Video settings" msgstr "" -#: src/view/com/composer/Composer.tsx:1751 +#: src/view/com/composer/Composer.tsx:1833 msgid "Video uploaded" msgstr "" @@ -9620,9 +9624,8 @@ msgstr "" msgid "Videos" msgstr "" -#: src/view/com/composer/videos/SelectVideoBtn.tsx:42 -#: src/view/com/composer/videos/SelectVideoBtn.tsx:55 -msgid "Videos must be less than 3 minutes long" +#: src/view/com/composer/SelectMediaButton.tsx:407 +msgid "Videos must be less than 3 minutes long." msgstr "" #: src/screens/Profile/Header/Shell.tsx:229 @@ -9707,11 +9710,11 @@ msgstr "" msgid "View video" msgstr "" -#: src/screens/Moderation/index.tsx:277 +#: src/screens/Moderation/index.tsx:296 msgid "View your blocked accounts" msgstr "" -#: src/screens/Moderation/index.tsx:217 +#: src/screens/Moderation/index.tsx:236 msgid "View your default post interaction settings" msgstr "" @@ -9720,11 +9723,11 @@ msgstr "" msgid "View your feeds and explore more" msgstr "" -#: src/screens/Moderation/index.tsx:247 +#: src/screens/Moderation/index.tsx:266 msgid "View your moderation lists" msgstr "" -#: src/screens/Moderation/index.tsx:262 +#: src/screens/Moderation/index.tsx:281 msgid "View your muted accounts" msgstr "" @@ -9829,7 +9832,7 @@ msgstr "" msgid "We were unable to load your birth date preferences. Please try again." msgstr "" -#: src/screens/Moderation/index.tsx:464 +#: src/screens/Moderation/index.tsx:483 msgid "We were unable to load your configured labelers at this time." msgstr "" @@ -9894,7 +9897,7 @@ msgstr "" msgid "We're sorry, but your search could not be completed. Please try again in a few minutes." msgstr "" -#: src/view/com/composer/Composer.tsx:460 +#: src/view/com/composer/Composer.tsx:468 msgid "We're sorry! The post you are replying to has been deleted." msgstr "" @@ -9945,7 +9948,7 @@ msgstr "" #: src/view/com/auth/SplashScreen.tsx:38 #: src/view/com/auth/SplashScreen.web.tsx:99 -#: src/view/com/composer/Composer.tsx:781 +#: src/view/com/composer/Composer.tsx:794 msgid "What's up?" msgstr "" @@ -10027,11 +10030,11 @@ msgstr "" msgid "Write a message" msgstr "" -#: src/view/com/composer/Composer.tsx:876 +#: src/view/com/composer/Composer.tsx:894 msgid "Write post" msgstr "" -#: src/view/com/composer/Composer.tsx:779 +#: src/view/com/composer/Composer.tsx:792 #: src/view/com/post-thread/PostThreadComposePrompt.tsx:90 msgid "Write your reply" msgstr "" @@ -10170,10 +10173,23 @@ msgstr "" msgid "You can now sign in with your new password." msgstr "" +#: src/view/com/composer/SelectMediaButton.tsx:410 +msgid "You can only select one GIF at a time." +msgstr "" + +#: src/view/com/composer/SelectMediaButton.tsx:404 +msgid "You can only select one video at a time." +msgstr "" + #: src/screens/Deactivated.tsx:133 msgid "You can reactivate your account to continue logging in. Your profile and posts will be visible to other users." msgstr "" +#. Error message for maximum number of images that can be selected to add to a post, currently 4 but may change. +#: src/view/com/composer/SelectMediaButton.tsx:396 +msgid "You can select up to {MAX_IMAGES, plural, other {# images}} in total." +msgstr "" + #: src/components/dialogs/PostInteractionSettingsDialog.tsx:85 msgid "You can set default interaction settings in <0>Settings → Moderation → Interaction settings." msgstr "" @@ -10316,10 +10332,6 @@ msgstr "" msgid "You may only add up to 3 feeds" msgstr "" -#: src/lib/media/picker.shared.ts:25 -msgid "You may only select up to 4 images" -msgstr "" - #: src/screens/Signup/StepInfo/Policies.tsx:136 msgid "You must be 13 years of age or older to create an account." msgstr "" @@ -10328,7 +10340,7 @@ msgstr "" msgid "You must be following at least seven other people to generate a starter pack." msgstr "" -#: src/screens/Moderation/index.tsx:355 +#: src/screens/Moderation/index.tsx:374 msgid "You must complete age assurance in order to access the settings below." msgstr "" @@ -10349,6 +10361,10 @@ msgstr "" msgid "You must sign in to view this post." msgstr "" +#: src/view/com/composer/SelectMediaButton.tsx:439 +msgid "You need to allow access to your media library." +msgstr "" + #: src/components/dialogs/EmailDialog/screens/Manage2FA/index.tsx:23 msgid "You need to verify your email address before you can enable email 2FA." msgstr "" @@ -10524,6 +10540,10 @@ msgstr "" msgid "Your current handle <0>{0} will automatically remain reserved for you. You can switch back to it at any time from this account." msgstr "" +#: src/screens/Moderation/index.tsx:208 +msgid "Your declared age is under 18. Some settings below may be disabled. If this was a mistake, you may edit your bithdate in your <0>account settings." +msgstr "" + #: src/components/ageAssurance/AgeAssuranceInitDialog.tsx:253 #: src/components/ageAssurance/AgeAssuranceInitDialog.tsx:257 #: src/components/ageAssurance/AgeAssuranceInitDialog.tsx:258 @@ -10586,11 +10606,11 @@ msgstr "" msgid "Your password must be at least 8 characters long." msgstr "" -#: src/view/com/composer/Composer.tsx:522 +#: src/view/com/composer/Composer.tsx:530 msgid "Your post has been published" msgstr "" -#: src/view/com/composer/Composer.tsx:519 +#: src/view/com/composer/Composer.tsx:527 msgid "Your posts have been published" msgstr "" @@ -10606,7 +10626,7 @@ msgstr "" msgid "Your profile, posts, feeds, and lists will no longer be visible to other Bluesky users. You can reactivate your account at any time by logging in." msgstr "" -#: src/view/com/composer/Composer.tsx:521 +#: src/view/com/composer/Composer.tsx:529 msgid "Your reply has been published" msgstr "" From 492378692776d21925fa3507034088371f712c16 Mon Sep 17 00:00:00 2001 From: Ridley Combs Date: Tue, 19 Aug 2025 06:25:42 -0700 Subject: [PATCH 4/6] [Lightbox] prevent browser-default escape key behavior (#8859) Some browsers exit fullscreen when the escape key is pressed without `preventDefault()` being called; we don't want that when we're handling the press ourselves. --- src/view/com/lightbox/Lightbox.web.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/view/com/lightbox/Lightbox.web.tsx b/src/view/com/lightbox/Lightbox.web.tsx index 97811da7f1..ab50fbcf05 100644 --- a/src/view/com/lightbox/Lightbox.web.tsx +++ b/src/view/com/lightbox/Lightbox.web.tsx @@ -76,6 +76,7 @@ function LightboxInner({ const onKeyDown = useCallback( (e: KeyboardEvent) => { if (e.key === 'Escape') { + e.preventDefault() onClose() } else if (e.key === 'ArrowLeft') { onPressLeft() From ea7f984a405c287f3fb50c151a78a6836e101347 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Tue, 19 Aug 2025 11:33:46 -0500 Subject: [PATCH 5/6] Fix isPartOfLastBranchFromDepth assignment (#8856) --- src/state/queries/usePostThread/traversal.ts | 11 +++++++++-- src/state/queries/usePostThread/types.ts | 4 ++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/state/queries/usePostThread/traversal.ts b/src/state/queries/usePostThread/traversal.ts index 2809d32e98..2e7693fab3 100644 --- a/src/state/queries/usePostThread/traversal.ts +++ b/src/state/queries/usePostThread/traversal.ts @@ -307,9 +307,16 @@ export function sortAndAnnotateThreadItems( metadata.isPartOfLastBranchFromDepth = metadata.depth /** - * If the parent is part of the last branch of the sub-tree, so is the child. + * If the parent is part of the last branch of the sub-tree, so + * is the child. However, if the child is also a last sibling, + * then we need to start tracking `isPartOfLastBranchFromDepth` + * from this point onwards, always updating it to the depth of + * the last sibling as we go down. */ - if (metadata.parentMetadata.isPartOfLastBranchFromDepth) { + if ( + !metadata.isLastSibling && + metadata.parentMetadata.isPartOfLastBranchFromDepth + ) { metadata.isPartOfLastBranchFromDepth = metadata.parentMetadata.isPartOfLastBranchFromDepth } diff --git a/src/state/queries/usePostThread/types.ts b/src/state/queries/usePostThread/types.ts index 2f370b0ab7..5df7c2e42e 100644 --- a/src/state/queries/usePostThread/types.ts +++ b/src/state/queries/usePostThread/types.ts @@ -151,8 +151,8 @@ export type TraversalMetadata = { */ isLastChild: boolean /** - * Indicates if the post is the left/lower-most branch of the reply tree. - * Value corresponds to the depth at which this branch started. + * Indicates if the post is the left-most AND lower-most branch of the reply + * tree. Value corresponds to the depth at which this branch started. */ isPartOfLastBranchFromDepth?: number /** From bf6e01551e496e6737dc627f8587cd545d236776 Mon Sep 17 00:00:00 2001 From: pfrazee <1270099+pfrazee@users.noreply.github.com> Date: Wed, 20 Aug 2025 02:38:26 +0000 Subject: [PATCH 6/6] Nightly source-language update --- src/locale/locales/en/messages.po | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/locale/locales/en/messages.po b/src/locale/locales/en/messages.po index 725d03495d..94bb386baf 100644 --- a/src/locale/locales/en/messages.po +++ b/src/locale/locales/en/messages.po @@ -1905,7 +1905,7 @@ msgstr "" msgid "Close image" msgstr "" -#: src/view/com/lightbox/Lightbox.web.tsx:109 +#: src/view/com/lightbox/Lightbox.web.tsx:110 msgid "Close image viewer" msgstr "" @@ -3207,11 +3207,11 @@ msgstr "" msgid "Exits image cropping process" msgstr "" -#: src/view/com/lightbox/Lightbox.web.tsx:110 +#: src/view/com/lightbox/Lightbox.web.tsx:111 msgid "Exits image view" msgstr "" -#: src/view/com/lightbox/Lightbox.web.tsx:184 +#: src/view/com/lightbox/Lightbox.web.tsx:185 msgid "Expand alt text" msgstr "" @@ -4234,7 +4234,7 @@ msgstr "" msgid "I understand" msgstr "" -#: src/view/com/lightbox/Lightbox.web.tsx:186 +#: src/view/com/lightbox/Lightbox.web.tsx:187 msgid "If alt text is long, toggles alt text expanded state" msgstr "" @@ -5444,7 +5444,7 @@ msgstr "" msgid "Next" msgstr "" -#: src/view/com/lightbox/Lightbox.web.tsx:169 +#: src/view/com/lightbox/Lightbox.web.tsx:170 msgid "Next image" msgstr "" @@ -6451,7 +6451,7 @@ msgstr "" msgid "Press to view followers of this account that you also follow" msgstr "" -#: src/view/com/lightbox/Lightbox.web.tsx:150 +#: src/view/com/lightbox/Lightbox.web.tsx:151 msgid "Previous image" msgstr ""