Merge branch 'main' into safelink

This commit is contained in:
Hailey
2025-08-20 09:24:31 -07:00
22 changed files with 934 additions and 443 deletions
+1
View File
@@ -58,6 +58,7 @@ buck-out/
# Ruby / CocoaPods # Ruby / CocoaPods
/ios/Pods/ /ios/Pods/
/vendor/bundle/ /vendor/bundle/
Gemfile.lock
# Testing # Testing
coverage/ coverage/
+1 -1
View File
@@ -146,7 +146,7 @@
"expo-image": "^2.4.0", "expo-image": "^2.4.0",
"expo-image-crop-tool": "^0.1.8", "expo-image-crop-tool": "^0.1.8",
"expo-image-manipulator": "~13.1.7", "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-intent-launcher": "^12.1.5",
"expo-linear-gradient": "~14.1.5", "expo-linear-gradient": "~14.1.5",
"expo-linking": "~7.1.5", "expo-linking": "~7.1.5",
-38
View File
@@ -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) {
@@ -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.
@@ -146,6 +146,8 @@ export function Scrubber({
const progress = scrubberActive ? seekPosition : currentTime const progress = scrubberActive ? seekPosition : currentTime
const progressPercent = (progress / duration) * 100 const progressPercent = (progress / duration) * 100
if (duration < 3) return null
return ( return (
<View <View
testID="scrubber" testID="scrubber"
@@ -373,6 +373,7 @@ export function Controls({
onPress={onPressPlayPause} onPress={onPressPlayPause}
/> />
<View style={a.flex_1} /> <View style={a.flex_1} />
{Math.round(duration) > 0 && (
<Text <Text
style={[ style={[
a.px_xs, a.px_xs,
@@ -380,6 +381,7 @@ export function Controls({
]}> ]}>
{formatTime(currentTime)} / {formatTime(duration)} {formatTime(currentTime)} / {formatTime(duration)}
</Text> </Text>
)}
{hasSubtitleTrack && ( {hasSubtitleTrack && (
<ControlButton <ControlButton
active={subtitlesEnabled} active={subtitlesEnabled}
+4
View File
@@ -181,6 +181,10 @@ export const VIDEO_SERVICE = 'https://video.bsky.app'
export const VIDEO_SERVICE_DID = 'did:web:video.bsky.app' export const VIDEO_SERVICE_DID = 'did:web:video.bsky.app'
export const VIDEO_MAX_DURATION_MS = 3 * 60 * 1000 // 3 minutes in milliseconds export const VIDEO_MAX_DURATION_MS = 3 * 60 * 1000 // 3 minutes in milliseconds
/**
* Maximum size of a video in megabytes, _not_ mebibytes. Backend uses
* ISO megabytes.
*/
export const VIDEO_MAX_SIZE = 1000 * 1000 * 100 // 100mb export const VIDEO_MAX_SIZE = 1000 * 1000 * 100 // 100mb
export const SUPPORTED_MIME_TYPES = [ export const SUPPORTED_MIME_TYPES = [
+2 -2
View File
@@ -4,7 +4,6 @@ import {impactAsync, ImpactFeedbackStyle} from 'expo-haptics'
import {isIOS, isWeb} from '#/platform/detection' import {isIOS, isWeb} from '#/platform/detection'
import {useHapticsDisabled} from '#/state/preferences/disable-haptics' import {useHapticsDisabled} from '#/state/preferences/disable-haptics'
import * as Toast from '#/view/com/util/Toast'
export function useHaptics() { export function useHaptics() {
const isHapticsDisabled = useHapticsDisabled() const isHapticsDisabled = useHapticsDisabled()
@@ -23,7 +22,8 @@ export function useHaptics() {
// DEV ONLY - show a toast when a haptic is meant to fire on simulator // DEV ONLY - show a toast when a haptic is meant to fire on simulator
if (__DEV__ && !Device.isDevice) { if (__DEV__ && !Device.isDevice) {
Toast.show(`Buzzz!`) // disabled because it's annoying
// Toast.show(`Buzzz!`)
} }
}, },
[isHapticsDisabled], [isHapticsDisabled],
+1 -5
View File
@@ -17,16 +17,12 @@ export async function openPicker(opts?: ImagePickerOptions) {
exif: false, exif: false,
mediaTypes: ['images'], mediaTypes: ['images'],
quality: 1, quality: 1,
selectionLimit: 1,
...opts, ...opts,
legacy: true, legacy: true,
}) })
if (response.assets && response.assets.length > 4) {
Toast.show(t`You may only select up to 4 images`, 'exclamation-circle')
}
return (response.assets ?? []) return (response.assets ?? [])
.slice(0, 4)
.filter(asset => { .filter(asset => {
if (asset.mimeType?.startsWith('image/')) return true if (asset.mimeType?.startsWith('image/')) return true
Toast.show(t`Only image files are supported`, 'exclamation-circle') Toast.show(t`Only image files are supported`, 'exclamation-circle')
+10 -3
View File
@@ -1,8 +1,8 @@
import {getVideoMetaData, Video} from 'react-native-compressor' 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 {SUPPORTED_MIME_TYPES, type SupportedMimeTypes} from '#/lib/constants'
import {CompressedVideo} from './types' import {type CompressedVideo} from './types'
import {extToMime} from './util' import {extToMime} from './util'
const MIN_SIZE_FOR_COMPRESSION = 25 // 25mb const MIN_SIZE_FOR_COMPRESSION = 25 // 25mb
@@ -20,6 +20,13 @@ export async function compressVideo(
file.mimeType as SupportedMimeTypes, 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 const minimumFileSizeForCompress = isAcceptableFormat
? MIN_SIZE_FOR_COMPRESSION ? MIN_SIZE_FOR_COMPRESSION
: 0 : 0
+140 -120
View File
@@ -153,7 +153,7 @@ msgstr ""
msgid "{0} joined this week" msgid "{0} joined this week"
msgstr "" 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}" msgid "{0} of {1}"
msgstr "" msgstr ""
@@ -687,7 +687,7 @@ msgstr ""
msgid "Add another account" msgid "Add another account"
msgstr "" msgstr ""
#: src/view/com/composer/Composer.tsx:780 #: src/view/com/composer/Composer.tsx:793
msgid "Add another post" msgid "Add another post"
msgstr "" msgstr ""
@@ -705,6 +705,11 @@ msgstr ""
msgid "Add emoji reaction" msgid "Add emoji reaction"
msgstr "" 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:403
#: src/components/moderation/ReportDialog/index.tsx:407 #: src/components/moderation/ReportDialog/index.tsx:407
msgid "Add more details (optional)" msgid "Add more details (optional)"
@@ -718,7 +723,7 @@ msgstr ""
msgid "Add muted words and tags" msgid "Add muted words and tags"
msgstr "" msgstr ""
#: src/view/com/composer/Composer.tsx:1344 #: src/view/com/composer/Composer.tsx:1426
msgid "Add new post" msgid "Add new post"
msgstr "" msgstr ""
@@ -788,7 +793,7 @@ msgstr ""
msgid "Adult Content" msgid "Adult Content"
msgstr "" 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</0>." msgid "Adult content can only be enabled via the Web at <0>bsky.app</0>."
msgstr "" msgstr ""
@@ -801,7 +806,7 @@ msgstr ""
msgid "Adult Content labels" msgid "Adult Content labels"
msgstr "" msgstr ""
#: src/screens/Moderation/index.tsx:454 #: src/screens/Moderation/index.tsx:473
msgid "Advanced" msgid "Advanced"
msgstr "" msgstr ""
@@ -888,7 +893,7 @@ msgstr ""
#: src/screens/Settings/AccessibilitySettings.tsx:54 #: src/screens/Settings/AccessibilitySettings.tsx:54
#: src/view/com/composer/GifAltText.tsx:154 #: 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:40
#: src/view/com/composer/videos/SubtitleDialog.tsx:55 #: src/view/com/composer/videos/SubtitleDialog.tsx:55
#: src/view/com/composer/videos/SubtitleDialog.tsx:101 #: 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 "" msgstr ""
#: src/view/com/composer/GifAltText.tsx:179 #: 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.}}" msgid "Alt text will be truncated. {MAX_ALT_TEXT, plural, other {Limit: {0} characters.}}"
msgstr "" msgstr ""
@@ -917,7 +922,7 @@ msgstr ""
msgid "An error has occurred" msgid "An error has occurred"
msgstr "" 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" msgid "An error occurred"
msgstr "" msgstr ""
@@ -945,10 +950,6 @@ msgstr ""
msgid "An error occurred while saving the QR code!" msgid "An error occurred while saving the QR code!"
msgstr "" 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:352
#: src/screens/StarterPack/StarterPackScreen.tsx:374 #: src/screens/StarterPack/StarterPackScreen.tsx:374
msgid "An error occurred while trying to follow all" 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?" msgid "Are you sure you want to remove this from your feeds?"
msgstr "" 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?" msgid "Are you sure you'd like to discard this draft?"
msgstr "" 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?" msgid "Are you sure you'd like to discard this post?"
msgstr "" msgstr ""
@@ -1337,7 +1338,7 @@ msgstr ""
msgid "Blocked" msgid "Blocked"
msgstr "" msgstr ""
#: src/screens/Moderation/index.tsx:282 #: src/screens/Moderation/index.tsx:301
msgid "Blocked accounts" msgid "Blocked accounts"
msgstr "" msgstr ""
@@ -1557,8 +1558,8 @@ msgstr ""
#: src/screens/Settings/Settings.tsx:289 #: src/screens/Settings/Settings.tsx:289
#: src/screens/Takendown.tsx:99 #: src/screens/Takendown.tsx:99
#: src/screens/Takendown.tsx:102 #: src/screens/Takendown.tsx:102
#: src/view/com/composer/Composer.tsx:969 #: src/view/com/composer/Composer.tsx:987
#: src/view/com/composer/Composer.tsx:980 #: src/view/com/composer/Composer.tsx:998
#: src/view/com/composer/photos/EditImageDialog.web.tsx:43 #: src/view/com/composer/photos/EditImageDialog.web.tsx:43
#: src/view/com/composer/photos/EditImageDialog.web.tsx:52 #: src/view/com/composer/photos/EditImageDialog.web.tsx:52
#: src/view/com/modals/ChangePassword.tsx:279 #: src/view/com/modals/ChangePassword.tsx:279
@@ -1904,7 +1905,7 @@ msgstr ""
msgid "Close image" msgid "Close image"
msgstr "" msgstr ""
#: src/view/com/lightbox/Lightbox.web.tsx:109 #: src/view/com/lightbox/Lightbox.web.tsx:110
msgid "Close image viewer" msgid "Close image viewer"
msgstr "" msgstr ""
@@ -1922,7 +1923,7 @@ msgstr ""
msgid "Closes password update alert" msgid "Closes password update alert"
msgstr "" msgstr ""
#: src/view/com/composer/Composer.tsx:977 #: src/view/com/composer/Composer.tsx:995
msgid "Closes post composer and discards post draft" msgid "Closes post composer and discards post draft"
msgstr "" msgstr ""
@@ -1980,7 +1981,7 @@ msgstr ""
msgid "Compose new post" msgid "Compose new post"
msgstr "" 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" msgid "Compose posts up to {0, plural, other {# characters}} in length"
msgstr "" msgstr ""
@@ -1988,7 +1989,7 @@ msgstr ""
msgid "Compose reply" msgid "Compose reply"
msgstr "" msgstr ""
#: src/view/com/composer/Composer.tsx:1738 #: src/view/com/composer/Composer.tsx:1820
msgid "Compressing video..." msgid "Compressing video..."
msgstr "" msgstr ""
@@ -2015,11 +2016,11 @@ msgstr ""
msgid "Confirm delete account" msgid "Confirm delete account"
msgstr "" msgstr ""
#: src/screens/Moderation/index.tsx:330 #: src/screens/Moderation/index.tsx:349
msgid "Confirm your age:" msgid "Confirm your age:"
msgstr "" msgstr ""
#: src/screens/Moderation/index.tsx:321 #: src/screens/Moderation/index.tsx:340
msgid "Confirm your birthdate" msgid "Confirm your birthdate"
msgstr "" msgstr ""
@@ -2072,8 +2073,8 @@ msgstr ""
msgid "Content Blocked" msgid "Content Blocked"
msgstr "" msgstr ""
#: src/screens/Moderation/index.tsx:317 #: src/screens/Moderation/index.tsx:336
#: src/screens/Moderation/index.tsx:351 #: src/screens/Moderation/index.tsx:370
msgid "Content filters" msgid "Content filters"
msgstr "" msgstr ""
@@ -2518,7 +2519,7 @@ msgstr ""
#: src/components/PostControls/PostMenu/PostMenuItems.tsx:678 #: src/components/PostControls/PostMenu/PostMenuItems.tsx:678
#: src/components/PostControls/PostMenu/PostMenuItems.tsx:680 #: src/components/PostControls/PostMenu/PostMenuItems.tsx:680
#: src/view/com/composer/Composer.tsx:888 #: src/view/com/composer/Composer.tsx:906
msgid "Delete post" msgid "Delete post"
msgstr "" msgstr ""
@@ -2566,7 +2567,7 @@ msgid "Description"
msgstr "" msgstr ""
#: src/view/com/composer/GifAltText.tsx:150 #: 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" msgid "Descriptive alt text"
msgstr "" msgstr ""
@@ -2620,7 +2621,7 @@ msgstr ""
msgid "Disable haptic feedback" msgid "Disable haptic feedback"
msgstr "" 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" msgid "Disable subtitles"
msgstr "" msgstr ""
@@ -2629,13 +2630,13 @@ msgstr ""
#: src/lib/moderation/useLabelBehaviorDescription.ts:68 #: src/lib/moderation/useLabelBehaviorDescription.ts:68
#: src/screens/Messages/Settings.tsx:155 #: src/screens/Messages/Settings.tsx:155
#: src/screens/Messages/Settings.tsx:158 #: src/screens/Messages/Settings.tsx:158
#: src/screens/Moderation/index.tsx:394 #: src/screens/Moderation/index.tsx:413
msgid "Disabled" msgid "Disabled"
msgstr "" msgstr ""
#: src/screens/Profile/Header/EditProfileDialog.tsx:89 #: src/screens/Profile/Header/EditProfileDialog.tsx:89
#: src/view/com/composer/Composer.tsx:731 #: src/view/com/composer/Composer.tsx:743
#: src/view/com/composer/Composer.tsx:921 #: src/view/com/composer/Composer.tsx:939
msgid "Discard" msgid "Discard"
msgstr "" msgstr ""
@@ -2643,11 +2644,11 @@ msgstr ""
msgid "Discard changes?" msgid "Discard changes?"
msgstr "" msgstr ""
#: src/view/com/composer/Composer.tsx:728 #: src/view/com/composer/Composer.tsx:740
msgid "Discard draft?" msgid "Discard draft?"
msgstr "" msgstr ""
#: src/view/com/composer/Composer.tsx:913 #: src/view/com/composer/Composer.tsx:931
msgid "Discard post?" msgid "Discard post?"
msgstr "" msgstr ""
@@ -2673,7 +2674,7 @@ msgstr ""
msgid "Dismiss" msgid "Dismiss"
msgstr "" msgstr ""
#: src/view/com/composer/Composer.tsx:1662 #: src/view/com/composer/Composer.tsx:1744
msgid "Dismiss error" msgid "Dismiss error"
msgstr "" msgstr ""
@@ -3002,7 +3003,7 @@ msgstr ""
msgid "Enable {0} only" msgid "Enable {0} only"
msgstr "" msgstr ""
#: src/screens/Moderation/index.tsx:381 #: src/screens/Moderation/index.tsx:400
msgid "Enable adult content" msgid "Enable adult content"
msgstr "" msgstr ""
@@ -3028,7 +3029,7 @@ msgstr ""
msgid "Enable push notifications" msgid "Enable push notifications"
msgstr "" 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" msgid "Enable subtitles"
msgstr "" msgstr ""
@@ -3048,7 +3049,7 @@ msgstr ""
#: src/screens/Messages/Settings.tsx:146 #: src/screens/Messages/Settings.tsx:146
#: src/screens/Messages/Settings.tsx:149 #: src/screens/Messages/Settings.tsx:149
#: src/screens/Moderation/index.tsx:392 #: src/screens/Moderation/index.tsx:411
msgid "Enabled" msgid "Enabled"
msgstr "" msgstr ""
@@ -3074,7 +3075,7 @@ msgstr ""
msgid "Enter code" msgid "Enter code"
msgstr "" 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" msgid "Enter fullscreen"
msgstr "" msgstr ""
@@ -3119,7 +3120,7 @@ msgstr ""
msgid "Entertainment" msgid "Entertainment"
msgstr "" msgstr ""
#: src/view/com/composer/Composer.tsx:1747 #: src/view/com/composer/Composer.tsx:1829
#: src/view/com/util/error/ErrorScreen.tsx:42 #: src/view/com/util/error/ErrorScreen.tsx:42
msgid "Error" msgid "Error"
msgstr "" msgstr ""
@@ -3194,7 +3195,7 @@ msgstr ""
msgid "Excludes users you follow" msgid "Excludes users you follow"
msgstr "" 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" msgid "Exit fullscreen"
msgstr "" msgstr ""
@@ -3206,11 +3207,11 @@ msgstr ""
msgid "Exits image cropping process" msgid "Exits image cropping process"
msgstr "" msgstr ""
#: src/view/com/lightbox/Lightbox.web.tsx:110 #: src/view/com/lightbox/Lightbox.web.tsx:111
msgid "Exits image view" msgid "Exits image view"
msgstr "" msgstr ""
#: src/view/com/lightbox/Lightbox.web.tsx:184 #: src/view/com/lightbox/Lightbox.web.tsx:185
msgid "Expand alt text" msgid "Expand alt text"
msgstr "" msgstr ""
@@ -3810,10 +3811,6 @@ msgctxt "from-feed"
msgid "From <0/>" msgid "From <0/>"
msgstr "" msgstr ""
#: src/view/com/composer/photos/SelectPhotoBtn.tsx:50
msgid "Gallery"
msgstr ""
#: src/components/StarterPack/ProfileStarterPacks.tsx:307 #: src/components/StarterPack/ProfileStarterPacks.tsx:307
msgid "Generate a starter pack" msgid "Generate a starter pack"
msgstr "" msgstr ""
@@ -3967,6 +3964,7 @@ msgstr ""
#: src/components/ageAssurance/AgeAssuranceAdmonition.tsx:89 #: src/components/ageAssurance/AgeAssuranceAdmonition.tsx:89
#: src/components/ageAssurance/AgeRestrictedScreen.tsx:75 #: src/components/ageAssurance/AgeRestrictedScreen.tsx:75
#: src/components/ageAssurance/AgeRestrictedScreen.tsx:84 #: src/components/ageAssurance/AgeRestrictedScreen.tsx:84
#: src/screens/Moderation/index.tsx:214
msgid "Go to account settings" msgid "Go to account settings"
msgstr "" msgstr ""
@@ -4176,7 +4174,7 @@ msgstr ""
msgid "Hmm, we're having trouble finding this feed. It may have been deleted." msgid "Hmm, we're having trouble finding this feed. It may have been deleted."
msgstr "" 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." msgid "Hmmmm, it seems we're having trouble loading this data. See below for more details. If this issue persists, please contact us."
msgstr "" msgstr ""
@@ -4236,7 +4234,7 @@ msgstr ""
msgid "I understand" msgid "I understand"
msgstr "" 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" msgid "If alt text is long, toggles alt text expanded state"
msgstr "" msgstr ""
@@ -4382,7 +4380,7 @@ msgstr ""
msgid "Interaction limited" msgid "Interaction limited"
msgstr "" msgstr ""
#: src/screens/Moderation/index.tsx:222 #: src/screens/Moderation/index.tsx:241
msgid "Interaction settings" msgid "Interaction settings"
msgstr "" msgstr ""
@@ -4451,7 +4449,7 @@ msgstr ""
msgid "It's just you right now! Add more people to your starter pack by searching above." msgid "It's just you right now! Add more people to your starter pack by searching above."
msgstr "" msgstr ""
#: src/view/com/composer/Composer.tsx:1681 #: src/view/com/composer/Composer.tsx:1763
msgid "Job ID: {0}" msgid "Job ID: {0}"
msgstr "" msgstr ""
@@ -4927,7 +4925,7 @@ msgstr ""
msgid "Manage saved feeds" msgid "Manage saved feeds"
msgstr "" msgstr ""
#: src/screens/Moderation/index.tsx:292 #: src/screens/Moderation/index.tsx:311
msgid "Manage verification settings" msgid "Manage verification settings"
msgstr "" msgstr ""
@@ -5045,7 +5043,7 @@ msgid "Misleading Post"
msgstr "" msgstr ""
#: src/Navigation.tsx:176 #: 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:188
#: src/screens/Settings/Settings.tsx:191 #: src/screens/Settings/Settings.tsx:191
msgid "Moderation" msgid "Moderation"
@@ -5079,7 +5077,7 @@ msgctxt "toast"
msgid "Moderation list updated" msgid "Moderation list updated"
msgstr "" msgstr ""
#: src/screens/Moderation/index.tsx:252 #: src/screens/Moderation/index.tsx:271
msgid "Moderation lists" msgid "Moderation lists"
msgstr "" msgstr ""
@@ -5096,7 +5094,7 @@ msgstr ""
msgid "Moderation states" msgid "Moderation states"
msgstr "" msgstr ""
#: src/screens/Moderation/index.tsx:206 #: src/screens/Moderation/index.tsx:225
msgid "Moderation tools" msgid "Moderation tools"
msgstr "" msgstr ""
@@ -5211,7 +5209,7 @@ msgstr ""
msgid "Mute words & tags" msgid "Mute words & tags"
msgstr "" msgstr ""
#: src/screens/Moderation/index.tsx:267 #: src/screens/Moderation/index.tsx:286
msgid "Muted accounts" msgid "Muted accounts"
msgstr "" msgstr ""
@@ -5228,7 +5226,7 @@ msgstr ""
msgid "Muted by \"{0}\"" msgid "Muted by \"{0}\""
msgstr "" msgstr ""
#: src/screens/Moderation/index.tsx:237 #: src/screens/Moderation/index.tsx:256
msgid "Muted words & tags" msgid "Muted words & tags"
msgstr "" msgstr ""
@@ -5446,7 +5444,7 @@ msgstr ""
msgid "Next" msgid "Next"
msgstr "" msgstr ""
#: src/view/com/lightbox/Lightbox.web.tsx:169 #: src/view/com/lightbox/Lightbox.web.tsx:170
msgid "Next image" msgid "Next image"
msgstr "" msgstr ""
@@ -5722,15 +5720,23 @@ msgstr ""
msgid "Onboarding reset" msgid "Onboarding reset"
msgstr "" msgstr ""
#: src/view/com/composer/Composer.tsx:347 #: src/view/com/composer/Composer.tsx:355
msgid "One or more GIFs is missing alt text." msgid "One or more GIFs is missing alt text."
msgstr "" msgstr ""
#: src/view/com/composer/Composer.tsx:344 #: src/view/com/composer/Composer.tsx:352
msgid "One or more images is missing alt text." msgid "One or more images is missing alt text."
msgstr "" 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." msgid "One or more videos is missing alt text."
msgstr "" msgstr ""
@@ -5748,7 +5754,7 @@ msgstr ""
msgid "Only followers who I follow" msgid "Only followers who I follow"
msgstr "" msgstr ""
#: src/lib/media/picker.shared.ts:32 #: src/lib/media/picker.shared.ts:28
msgid "Only image files are supported" msgid "Only image files are supported"
msgstr "" msgstr ""
@@ -5787,7 +5793,7 @@ msgid "Open drawer menu"
msgstr "" msgstr ""
#: src/screens/Messages/components/MessageInput.web.tsx:181 #: 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" msgid "Open emoji picker"
msgstr "" msgstr ""
@@ -5816,7 +5822,7 @@ msgstr ""
msgid "Open moderation debug page" msgid "Open moderation debug page"
msgstr "" msgstr ""
#: src/screens/Moderation/index.tsx:233 #: src/screens/Moderation/index.tsx:252
msgid "Open muted words and tags settings" msgid "Open muted words and tags settings"
msgstr "" msgstr ""
@@ -5886,11 +5892,17 @@ msgstr ""
msgid "Opens composer" msgid "Opens composer"
msgstr "" msgstr ""
#: src/view/com/composer/photos/SelectPhotoBtn.tsx:51 #. 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.
msgid "Opens device photo gallery" #: 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 "" 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" msgid "Opens emoji picker"
msgstr "" msgstr ""
@@ -5933,10 +5945,6 @@ msgstr ""
msgid "Opens this profile" msgid "Opens this profile"
msgstr "" msgstr ""
#: src/view/com/composer/videos/SelectVideoBtn.tsx:75
msgid "Opens video picker"
msgstr ""
#: src/components/dms/ReportDialog.tsx:221 #: src/components/dms/ReportDialog.tsx:221
#: src/components/ReportDialog/SubmitView.tsx:168 #: src/components/ReportDialog/SubmitView.tsx:168
msgid "Optionally provide additional information below:" msgid "Optionally provide additional information below:"
@@ -6310,12 +6318,12 @@ msgctxt "description"
msgid "Post" msgid "Post"
msgstr "" msgstr ""
#: src/view/com/composer/Composer.tsx:1040 #: src/view/com/composer/Composer.tsx:1058
msgctxt "action" msgctxt "action"
msgid "Post" msgid "Post"
msgstr "" msgstr ""
#: src/view/com/composer/Composer.tsx:1038 #: src/view/com/composer/Composer.tsx:1056
msgctxt "action" msgctxt "action"
msgid "Post All" msgid "Post All"
msgstr "" msgstr ""
@@ -6443,7 +6451,7 @@ msgstr ""
msgid "Press to view followers of this account that you also follow" msgid "Press to view followers of this account that you also follow"
msgstr "" msgstr ""
#: src/view/com/lightbox/Lightbox.web.tsx:150 #: src/view/com/lightbox/Lightbox.web.tsx:151
msgid "Previous image" msgid "Previous image"
msgstr "" msgstr ""
@@ -6490,7 +6498,7 @@ msgstr ""
msgid "Privacy Policy" msgid "Privacy Policy"
msgstr "" msgstr ""
#: src/view/com/composer/Composer.tsx:1744 #: src/view/com/composer/Composer.tsx:1826
msgid "Processing video..." msgid "Processing video..."
msgstr "" msgstr ""
@@ -6529,22 +6537,22 @@ msgid "Public, sharable lists which can be used to drive feeds."
msgstr "" msgstr ""
#. Accessibility label for button to publish a single post #. 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" msgid "Publish post"
msgstr "" msgstr ""
#. Accessibility label for button to publish multiple posts in a thread #. 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" msgid "Publish posts"
msgstr "" msgstr ""
#. Accessibility label for button to publish multiple replies in a thread #. 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" msgid "Publish replies"
msgstr "" msgstr ""
#. Accessibility label for button to publish a single reply #. 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" msgid "Publish reply"
msgstr "" msgstr ""
@@ -6911,7 +6919,7 @@ msgstr ""
msgid "Replies to this post are disabled." msgid "Replies to this post are disabled."
msgstr "" msgstr ""
#: src/view/com/composer/Composer.tsx:1036 #: src/view/com/composer/Composer.tsx:1054
msgctxt "action" msgctxt "action"
msgid "Reply" msgid "Reply"
msgstr "" msgstr ""
@@ -7239,8 +7247,8 @@ msgstr ""
#: src/view/com/composer/GifAltText.tsx:202 #: src/view/com/composer/GifAltText.tsx:202
#: src/view/com/composer/photos/EditImageDialog.web.tsx:62 #: src/view/com/composer/photos/EditImageDialog.web.tsx:62
#: src/view/com/composer/photos/EditImageDialog.web.tsx:75 #: src/view/com/composer/photos/EditImageDialog.web.tsx:75
#: src/view/com/composer/photos/ImageAltTextDialog.tsx:153 #: src/view/com/composer/photos/ImageAltTextDialog.tsx:152
#: src/view/com/composer/photos/ImageAltTextDialog.tsx:163 #: src/view/com/composer/photos/ImageAltTextDialog.tsx:162
#: src/view/com/modals/CreateOrEditList.tsx:315 #: src/view/com/modals/CreateOrEditList.tsx:315
#: src/view/screens/SavedFeeds.tsx:117 #: src/view/screens/SavedFeeds.tsx:117
msgid "Save" msgid "Save"
@@ -7440,7 +7448,7 @@ msgstr ""
msgid "See this guide" msgid "See this guide"
msgstr "" 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" msgid "Seek slider. Use the arrow keys to seek forwards and backwards, and space to play/pause"
msgstr "" msgstr ""
@@ -7539,10 +7547,6 @@ msgstr ""
msgid "Select the moderation service(s) to report to" msgid "Select the moderation service(s) to report to"
msgstr "" msgstr ""
#: src/view/com/composer/videos/SelectVideoBtn.tsx:74
msgid "Select video"
msgstr ""
#: src/components/dialogs/MutedWords.tsx:242 #: src/components/dialogs/MutedWords.tsx:242
msgid "Select what content this mute word should apply to." msgid "Select what content this mute word should apply to."
msgstr "" msgstr ""
@@ -7572,6 +7576,10 @@ msgstr ""
msgid "Select your preferred notification channels" msgid "Select your preferred notification channels"
msgstr "" 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 #: src/view/com/util/forms/DropdownButton.tsx:302
msgid "Selects option {0} of {numItems}" msgid "Selects option {0} of {numItems}"
msgstr "" msgstr ""
@@ -7649,7 +7657,7 @@ msgstr ""
msgid "Set app icon to {0}" msgid "Set app icon to {0}"
msgstr "" msgstr ""
#: src/screens/Moderation/index.tsx:333 #: src/screens/Moderation/index.tsx:352
msgid "Set birthdate" msgid "Set birthdate"
msgstr "" msgstr ""
@@ -8077,7 +8085,7 @@ msgid "Something went wrong, please try again"
msgstr "" msgstr ""
#: src/components/ReportDialog/index.tsx:54 #: 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 #: src/screens/Profile/Sections/Labels.tsx:184
msgid "Something went wrong, please try again." msgid "Something went wrong, please try again."
msgstr "" msgstr ""
@@ -8482,7 +8490,7 @@ msgstr ""
msgid "The author of this thread has hidden this reply." msgid "The author of this thread has hidden this reply."
msgstr "" msgstr ""
#: src/screens/Moderation/index.tsx:407 #: src/screens/Moderation/index.tsx:426
msgid "The Bluesky web application" msgid "The Bluesky web application"
msgstr "" msgstr ""
@@ -8840,7 +8848,7 @@ msgstr ""
msgid "This post will be hidden from feeds and threads. This cannot be undone." msgid "This post will be hidden from feeds and threads. This cannot be undone."
msgstr "" msgstr ""
#: src/view/com/composer/Composer.tsx:463 #: src/view/com/composer/Composer.tsx:471
msgid "This post's author has disabled quote posts." msgid "This post's author has disabled quote posts."
msgstr "" msgstr ""
@@ -8974,7 +8982,7 @@ msgstr ""
msgid "Toggle dropdown" msgid "Toggle dropdown"
msgstr "" msgstr ""
#: src/screens/Moderation/index.tsx:384 #: src/screens/Moderation/index.tsx:403
msgid "Toggle to enable or disable adult content" msgid "Toggle to enable or disable adult content"
msgstr "" msgstr ""
@@ -9256,12 +9264,8 @@ msgstr ""
msgid "Unsubscribed from list" msgid "Unsubscribed from list"
msgstr "" msgstr ""
#: src/view/com/composer/Composer.tsx:818 #: src/view/com/composer/Composer.tsx:834
msgid "Unsupported video type" msgid "Unsupported video type: {mimeType}"
msgstr ""
#: src/view/com/composer/videos/SelectVideoBtn.tsx:48
msgid "Unsupported video type: {0}"
msgstr "" msgstr ""
#: src/components/moderation/ReportDialog/utils/useReportOptions.ts:77 #: src/components/moderation/ReportDialog/utils/useReportOptions.ts:77
@@ -9346,7 +9350,7 @@ msgstr ""
msgid "Uploading link thumbnail..." msgid "Uploading link thumbnail..."
msgstr "" msgstr ""
#: src/view/com/composer/Composer.tsx:1741 #: src/view/com/composer/Composer.tsx:1823
msgid "Uploading video..." msgid "Uploading video..."
msgstr "" msgstr ""
@@ -9482,7 +9486,7 @@ msgstr ""
msgid "Verification failed, please try again." msgid "Verification failed, please try again."
msgstr "" msgstr ""
#: src/screens/Moderation/index.tsx:297 #: src/screens/Moderation/index.tsx:316
msgid "Verification settings" msgid "Verification settings"
msgstr "" msgstr ""
@@ -9608,7 +9612,7 @@ msgstr ""
msgid "Video settings" msgid "Video settings"
msgstr "" msgstr ""
#: src/view/com/composer/Composer.tsx:1751 #: src/view/com/composer/Composer.tsx:1833
msgid "Video uploaded" msgid "Video uploaded"
msgstr "" msgstr ""
@@ -9620,9 +9624,8 @@ msgstr ""
msgid "Videos" msgid "Videos"
msgstr "" msgstr ""
#: src/view/com/composer/videos/SelectVideoBtn.tsx:42 #: src/view/com/composer/SelectMediaButton.tsx:407
#: src/view/com/composer/videos/SelectVideoBtn.tsx:55 msgid "Videos must be less than 3 minutes long."
msgid "Videos must be less than 3 minutes long"
msgstr "" msgstr ""
#: src/screens/Profile/Header/Shell.tsx:229 #: src/screens/Profile/Header/Shell.tsx:229
@@ -9707,11 +9710,11 @@ msgstr ""
msgid "View video" msgid "View video"
msgstr "" msgstr ""
#: src/screens/Moderation/index.tsx:277 #: src/screens/Moderation/index.tsx:296
msgid "View your blocked accounts" msgid "View your blocked accounts"
msgstr "" msgstr ""
#: src/screens/Moderation/index.tsx:217 #: src/screens/Moderation/index.tsx:236
msgid "View your default post interaction settings" msgid "View your default post interaction settings"
msgstr "" msgstr ""
@@ -9720,11 +9723,11 @@ msgstr ""
msgid "View your feeds and explore more" msgid "View your feeds and explore more"
msgstr "" msgstr ""
#: src/screens/Moderation/index.tsx:247 #: src/screens/Moderation/index.tsx:266
msgid "View your moderation lists" msgid "View your moderation lists"
msgstr "" msgstr ""
#: src/screens/Moderation/index.tsx:262 #: src/screens/Moderation/index.tsx:281
msgid "View your muted accounts" msgid "View your muted accounts"
msgstr "" msgstr ""
@@ -9829,7 +9832,7 @@ msgstr ""
msgid "We were unable to load your birth date preferences. Please try again." msgid "We were unable to load your birth date preferences. Please try again."
msgstr "" 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." msgid "We were unable to load your configured labelers at this time."
msgstr "" msgstr ""
@@ -9894,7 +9897,7 @@ msgstr ""
msgid "We're sorry, but your search could not be completed. Please try again in a few minutes." msgid "We're sorry, but your search could not be completed. Please try again in a few minutes."
msgstr "" 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." msgid "We're sorry! The post you are replying to has been deleted."
msgstr "" msgstr ""
@@ -9945,7 +9948,7 @@ msgstr ""
#: src/view/com/auth/SplashScreen.tsx:38 #: src/view/com/auth/SplashScreen.tsx:38
#: src/view/com/auth/SplashScreen.web.tsx:99 #: 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?" msgid "What's up?"
msgstr "" msgstr ""
@@ -10027,11 +10030,11 @@ msgstr ""
msgid "Write a message" msgid "Write a message"
msgstr "" msgstr ""
#: src/view/com/composer/Composer.tsx:876 #: src/view/com/composer/Composer.tsx:894
msgid "Write post" msgid "Write post"
msgstr "" msgstr ""
#: src/view/com/composer/Composer.tsx:779 #: src/view/com/composer/Composer.tsx:792
#: src/view/com/post-thread/PostThreadComposePrompt.tsx:90 #: src/view/com/post-thread/PostThreadComposePrompt.tsx:90
msgid "Write your reply" msgid "Write your reply"
msgstr "" msgstr ""
@@ -10170,10 +10173,23 @@ msgstr ""
msgid "You can now sign in with your new password." msgid "You can now sign in with your new password."
msgstr "" 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 #: 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." msgid "You can reactivate your account to continue logging in. Your profile and posts will be visible to other users."
msgstr "" 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 #: src/components/dialogs/PostInteractionSettingsDialog.tsx:85
msgid "You can set default interaction settings in <0>Settings → Moderation → Interaction settings</0>." msgid "You can set default interaction settings in <0>Settings → Moderation → Interaction settings</0>."
msgstr "" msgstr ""
@@ -10316,10 +10332,6 @@ msgstr ""
msgid "You may only add up to 3 feeds" msgid "You may only add up to 3 feeds"
msgstr "" 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 #: src/screens/Signup/StepInfo/Policies.tsx:136
msgid "You must be 13 years of age or older to create an account." msgid "You must be 13 years of age or older to create an account."
msgstr "" msgstr ""
@@ -10328,7 +10340,7 @@ msgstr ""
msgid "You must be following at least seven other people to generate a starter pack." msgid "You must be following at least seven other people to generate a starter pack."
msgstr "" 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." msgid "You must complete age assurance in order to access the settings below."
msgstr "" msgstr ""
@@ -10349,6 +10361,10 @@ msgstr ""
msgid "You must sign in to view this post." msgid "You must sign in to view this post."
msgstr "" 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 #: src/components/dialogs/EmailDialog/screens/Manage2FA/index.tsx:23
msgid "You need to verify your email address before you can enable email 2FA." msgid "You need to verify your email address before you can enable email 2FA."
msgstr "" msgstr ""
@@ -10524,6 +10540,10 @@ msgstr ""
msgid "Your current handle <0>{0}</0> will automatically remain reserved for you. You can switch back to it at any time from this account." msgid "Your current handle <0>{0}</0> will automatically remain reserved for you. You can switch back to it at any time from this account."
msgstr "" 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</0>."
msgstr ""
#: src/components/ageAssurance/AgeAssuranceInitDialog.tsx:253 #: src/components/ageAssurance/AgeAssuranceInitDialog.tsx:253
#: src/components/ageAssurance/AgeAssuranceInitDialog.tsx:257 #: src/components/ageAssurance/AgeAssuranceInitDialog.tsx:257
#: src/components/ageAssurance/AgeAssuranceInitDialog.tsx:258 #: src/components/ageAssurance/AgeAssuranceInitDialog.tsx:258
@@ -10586,11 +10606,11 @@ msgstr ""
msgid "Your password must be at least 8 characters long." msgid "Your password must be at least 8 characters long."
msgstr "" msgstr ""
#: src/view/com/composer/Composer.tsx:522 #: src/view/com/composer/Composer.tsx:530
msgid "Your post has been published" msgid "Your post has been published"
msgstr "" msgstr ""
#: src/view/com/composer/Composer.tsx:519 #: src/view/com/composer/Composer.tsx:527
msgid "Your posts have been published" msgid "Your posts have been published"
msgstr "" 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." 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 "" msgstr ""
#: src/view/com/composer/Composer.tsx:521 #: src/view/com/composer/Composer.tsx:529
msgid "Your reply has been published" msgid "Your reply has been published"
msgstr "" msgstr ""
+19
View File
@@ -22,6 +22,7 @@ import {
import {isNonConfigurableModerationAuthority} from '#/state/session/additional-moderation-authorities' import {isNonConfigurableModerationAuthority} from '#/state/session/additional-moderation-authorities'
import {useSetMinimalShellMode} from '#/state/shell' import {useSetMinimalShellMode} from '#/state/shell'
import {atoms as a, useBreakpoints, useTheme, type ViewStyleProp} from '#/alf' import {atoms as a, useBreakpoints, useTheme, type ViewStyleProp} from '#/alf'
import {Admonition} from '#/components/Admonition'
import {AgeAssuranceAdmonition} from '#/components/ageAssurance/AgeAssuranceAdmonition' import {AgeAssuranceAdmonition} from '#/components/ageAssurance/AgeAssuranceAdmonition'
import {Button, ButtonText} from '#/components/Button' import {Button, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog' import * as Dialog from '#/components/Dialog'
@@ -201,6 +202,24 @@ export function ModerationScreenInner({
return ( return (
<View style={[a.pt_2xl, a.px_lg, gtMobile && a.px_2xl]}> <View style={[a.pt_2xl, a.px_lg, gtMobile && a.px_2xl]}>
{isDeclaredUnderage && (
<View style={[a.pb_2xl]}>
<Admonition type="tip" style={[a.pb_md]}>
<Trans>
Your declared age is under 18. Some settings below may be
disabled. If this was a mistake, you may edit your bithdate in
your{' '}
<InlineLinkText
to="/settings/account"
label={_(msg`Go to account settings`)}>
account settings
</InlineLinkText>
.
</Trans>
</Admonition>
</View>
)}
<Text <Text
style={[a.text_md, a.font_bold, a.pb_md, t.atoms.text_contrast_high]}> style={[a.text_md, a.font_bold, a.pb_md, t.atoms.text_contrast_high]}>
<Trans>Moderation tools</Trans> <Trans>Moderation tools</Trans>
+9 -2
View File
@@ -307,9 +307,16 @@ export function sortAndAnnotateThreadItems(
metadata.isPartOfLastBranchFromDepth = metadata.depth 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.isPartOfLastBranchFromDepth =
metadata.parentMetadata.isPartOfLastBranchFromDepth metadata.parentMetadata.isPartOfLastBranchFromDepth
} }
+2 -2
View File
@@ -151,8 +151,8 @@ export type TraversalMetadata = {
*/ */
isLastChild: boolean isLastChild: boolean
/** /**
* Indicates if the post is the left/lower-most branch of the reply tree. * Indicates if the post is the left-most AND lower-most branch of the reply
* Value corresponds to the depth at which this branch started. * tree. Value corresponds to the depth at which this branch started.
*/ */
isPartOfLastBranchFromDepth?: number isPartOfLastBranchFromDepth?: number
/** /**
+99 -17
View File
@@ -40,6 +40,7 @@ import Animated, {
ZoomIn, ZoomIn,
ZoomOut, ZoomOut,
} from 'react-native-reanimated' } from 'react-native-reanimated'
import {RootSiblingParent} from 'react-native-root-siblings'
import {useSafeAreaInsets} from 'react-native-safe-area-context' import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {type ImagePickerAsset} from 'expo-image-picker' import {type ImagePickerAsset} from 'expo-image-picker'
import { import {
@@ -77,7 +78,11 @@ import {logger} from '#/logger'
import {isAndroid, isIOS, isNative, isWeb} from '#/platform/detection' import {isAndroid, isIOS, isNative, isWeb} from '#/platform/detection'
import {useDialogStateControlContext} from '#/state/dialogs' import {useDialogStateControlContext} from '#/state/dialogs'
import {emitPostCreated} from '#/state/events' 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 {useModalControls} from '#/state/modals'
import {useRequireAltTextEnabled} from '#/state/preferences' import {useRequireAltTextEnabled} from '#/state/preferences'
import { import {
@@ -103,7 +108,6 @@ import {LabelsBtn} from '#/view/com/composer/labels/LabelsBtn'
import {Gallery} from '#/view/com/composer/photos/Gallery' import {Gallery} from '#/view/com/composer/photos/Gallery'
import {OpenCameraBtn} from '#/view/com/composer/photos/OpenCameraBtn' import {OpenCameraBtn} from '#/view/com/composer/photos/OpenCameraBtn'
import {SelectGifBtn} from '#/view/com/composer/photos/SelectGifBtn' 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 {SelectLangBtn} from '#/view/com/composer/select-language/SelectLangBtn'
import {SuggestedLanguage} from '#/view/com/composer/select-language/SuggestedLanguage' import {SuggestedLanguage} from '#/view/com/composer/select-language/SuggestedLanguage'
// TODO: Prevent naming components that coincide with RN primitives // TODO: Prevent naming components that coincide with RN primitives
@@ -113,12 +117,10 @@ import {
type TextInputRef, type TextInputRef,
} from '#/view/com/composer/text-input/TextInput' } from '#/view/com/composer/text-input/TextInput'
import {ThreadgateBtn} from '#/view/com/composer/threadgate/ThreadgateBtn' 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 {SubtitleDialogBtn} from '#/view/com/composer/videos/SubtitleDialog'
import {VideoPreview} from '#/view/com/composer/videos/VideoPreview' import {VideoPreview} from '#/view/com/composer/videos/VideoPreview'
import {VideoTranscodeProgress} from '#/view/com/composer/videos/VideoTranscodeProgress' import {VideoTranscodeProgress} from '#/view/com/composer/videos/VideoTranscodeProgress'
import {Text} from '#/view/com/util/text/Text' import {Text} from '#/view/com/util/text/Text'
import * as Toast from '#/view/com/util/Toast'
import {UserAvatar} from '#/view/com/util/UserAvatar' import {UserAvatar} from '#/view/com/util/UserAvatar'
import {atoms as a, native, useTheme, web} from '#/alf' import {atoms as a, native, useTheme, web} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button' 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 {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
import {LazyQuoteEmbed} from '#/components/Post/Embed/LazyQuoteEmbed' import {LazyQuoteEmbed} from '#/components/Post/Embed/LazyQuoteEmbed'
import * as Prompt from '#/components/Prompt' import * as Prompt from '#/components/Prompt'
import * as toast from '#/components/Toast'
import {Text as NewText} from '#/components/Typography' import {Text as NewText} from '#/components/Typography'
import {BottomSheetPortalProvider} from '../../../../modules/bottom-sheet' import {BottomSheetPortalProvider} from '../../../../modules/bottom-sheet'
import {
type AssetType,
SelectMediaButton,
type SelectMediaButtonProps,
} from './SelectMediaButton'
import { import {
type ComposerAction, type ComposerAction,
composerReducer, composerReducer,
@@ -514,12 +522,13 @@ export const ComposePost = ({
onPostSuccess?.(postSuccessData) onPostSuccess?.(postSuccessData)
} }
onClose() onClose()
Toast.show( toast.show(
thread.posts.length > 1 thread.posts.length > 1
? _(msg`Your posts have been published`) ? _(msg`Your posts have been published`)
: replyTo : replyTo
? _(msg`Your reply has been published`) ? _(msg`Your reply has been published`)
: _(msg`Your post has been published`), : _(msg`Your post has been published`),
{type: 'success'},
) )
}, [ }, [
_, _,
@@ -654,6 +663,7 @@ export const ComposePost = ({
const isWebFooterSticky = !isNative && thread.posts.length > 1 const isWebFooterSticky = !isNative && thread.posts.length > 1
return ( return (
<BottomSheetPortalProvider> <BottomSheetPortalProvider>
<RootSiblingParent>
<KeyboardAvoidingView <KeyboardAvoidingView
testID="composePostView" testID="composePostView"
behavior={isIOS ? 'padding' : 'height'} behavior={isIOS ? 'padding' : 'height'}
@@ -663,6 +673,7 @@ export const ComposePost = ({
style={[a.flex_1, viewStyles]} style={[a.flex_1, viewStyles]}
aria-modal aria-modal
accessibilityViewIsModal> accessibilityViewIsModal>
<RootSiblingParent>
<ComposerTopBar <ComposerTopBar
canPost={canPost} canPost={canPost}
isReply={!!replyTo} isReply={!!replyTo}
@@ -721,6 +732,7 @@ export const ComposePost = ({
))} ))}
</Animated.ScrollView> </Animated.ScrollView>
{!isWebFooterSticky && footer} {!isWebFooterSticky && footer}
</RootSiblingParent>
</View> </View>
<Prompt.Basic <Prompt.Basic
@@ -732,6 +744,7 @@ export const ComposePost = ({
confirmButtonColor="negative" confirmButtonColor="negative"
/> />
</KeyboardAvoidingView> </KeyboardAvoidingView>
</RootSiblingParent>
</BottomSheetPortalProvider> </BottomSheetPortalProvider>
) )
} }
@@ -811,11 +824,16 @@ let ComposerPost = React.memo(function ComposerPost({
const onPhotoPasted = useCallback( const onPhotoPasted = useCallback(
async (uri: string) => { 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 if (isNative) return // web only
const [mimeType] = uri.slice('data:'.length).split(';') const [mimeType] = uri.slice('data:'.length).split(';')
if (!SUPPORTED_MIME_TYPES.includes(mimeType as SupportedMimeTypes)) { 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 return
} }
const name = `pasted.${mimeToExt(mimeType)}` const name = `pasted.${mimeToExt(mimeType)}`
@@ -1251,7 +1269,6 @@ function ComposerFooter({
dispatch, dispatch,
showAddButton, showAddButton,
onEmojiButtonPress, onEmojiButtonPress,
onError,
onSelectVideo, onSelectVideo,
onAddPost, onAddPost,
}: { }: {
@@ -1266,11 +1283,32 @@ function ComposerFooter({
const t = useTheme() const t = useTheme()
const {_} = useLingui() const {_} = useLingui()
const {isMobile} = useWebMediaQueries() 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 media = post.embed.media
const images = media?.type === 'images' ? media.images : [] const images = media?.type === 'images' ? media.images : []
const video = media?.type === 'video' ? media.video : null const video = media?.type === 'video' ? media.video : null
const isMaxImages = images.length >= MAX_IMAGES 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( const onImageAdd = useCallback(
(next: ComposerImage[]) => { (next: ComposerImage[]) => {
@@ -1289,6 +1327,54 @@ function ComposerFooter({
[dispatch], [dispatch],
) )
/*
* Reset if the user clears any selected media
*/
if (selectedAssetsType !== undefined && !media) {
setSelectedAssetsType(undefined)
}
const onSelectAssets = useCallback<SelectMediaButtonProps['onSelectAssets']>(
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 ( return (
<View <View
style={[ style={[
@@ -1307,15 +1393,11 @@ function ComposerFooter({
<VideoUploadToolbar state={video} /> <VideoUploadToolbar state={video} />
) : ( ) : (
<ToolbarWrapper style={[a.flex_row, a.align_center, a.gap_xs]}> <ToolbarWrapper style={[a.flex_row, a.align_center, a.gap_xs]}>
<SelectPhotoBtn <SelectMediaButton
size={images.length} disabled={isMediaSelectionDisabled}
disabled={media?.type === 'images' ? isMaxImages : !!media} allowedAssetTypes={selectedAssetsType}
onAdd={onImageAdd} selectedAssetsCount={selectedAssetsCount}
/> onSelectAssets={onSelectAssets}
<SelectVideoBtn
onSelectVideo={asset => onSelectVideo(post.id, asset)}
disabled={!!media}
setError={onError}
/> />
<OpenCameraBtn <OpenCameraBtn
disabled={media?.type === 'images' ? isMaxImages : !!media} disabled={media?.type === 'images' ? isMaxImages : !!media}
+524
View File
@@ -0,0 +1,524 @@
import {useCallback} from 'react'
import {Keyboard} from 'react-native'
import {
type ImagePickerAsset,
launchImageLibraryAsync,
UIImagePickerPreferredAssetRepresentationMode,
} from 'expo-image-picker'
import {msg, plural} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {VIDEO_MAX_DURATION_MS, VIDEO_MAX_SIZE} from '#/lib/constants'
import {
usePhotoLibraryPermission,
useVideoLibraryPermission,
} from '#/lib/hooks/usePermissions'
import {extractDataUriMime} from '#/lib/media/util'
import {isIOS, isNative, isWeb} from '#/platform/detection'
import {MAX_IMAGES} from '#/view/com/composer/state/composer'
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 ImageIcon} from '#/components/icons/Image'
import * as toast from '#/components/Toast'
export type SelectMediaButtonProps = {
disabled?: boolean
/**
* If set, this limits the types of assets that can be selected.
*/
allowedAssetTypes: AssetType | undefined
selectedAssetsCount: number
onSelectAssets: (props: {
type: AssetType
assets: ImagePickerAsset[]
errors: string[]
}) => 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<ImagePickerAsset, 'mimeType'> & {
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<SelectedAssetError>()
/*
* 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 (
<Button
testID="openMediaBtn"
onPress={onPressSelectMedia}
label={_(
msg({
message: `Add media to post`,
comment: `Accessibility label for button in composer to add photos or a video to a post`,
}),
)}
accessibilityHint={
isNative
? _(
msg({
message: `Opens device gallery to select up to ${plural(
MAX_IMAGES,
{
other: '# images',
},
)}, or a single video.`,
comment: `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.`,
}),
)
: _(
msg({
message: `Opens device gallery to select up to ${plural(
MAX_IMAGES,
{
other: '# images',
},
)}, or a single video or GIF.`,
comment: `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.`,
}),
)
}
style={a.p_sm}
variant="ghost"
shape="round"
color="primary"
disabled={disabled}>
<ImageIcon
size="lg"
style={disabled && t.atoms.text_contrast_low}
accessibilityIgnoresInvertColors={true}
/>
</Button>
)
}
@@ -96,13 +96,12 @@ const ImageAltTextInner = ({
<View style={[t.atoms.bg_contrast_50, a.rounded_sm, a.overflow_hidden]}> <View style={[t.atoms.bg_contrast_50, a.rounded_sm, a.overflow_hidden]}>
<Image <Image
style={imageStyle} style={imageStyle}
source={{ source={{uri: (image.transformed ?? image.source).path}}
uri: (image.transformed ?? image.source).path,
}}
contentFit="contain" contentFit="contain"
accessible={true} accessible={true}
accessibilityIgnoresInvertColors accessibilityIgnoresInvertColors
enableLiveTextInteraction enableLiveTextInteraction
autoplay={false}
/> />
</View> </View>
</View> </View>
@@ -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 (
<Button
testID="openGalleryBtn"
onPress={onPressSelectPhotos}
label={_(msg`Gallery`)}
accessibilityHint={_(msg`Opens device photo gallery`)}
style={a.p_sm}
variant="ghost"
shape="round"
color="primary"
disabled={disabled}>
<Image size="lg" style={disabled && t.atoms.text_contrast_low} />
</Button>
)
}
@@ -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 (
<>
<Button
testID="openGifBtn"
onPress={onPressSelectVideo}
label={_(msg`Select video`)}
accessibilityHint={_(msg`Opens video picker`)}
style={a.p_sm}
variant="ghost"
shape="round"
color="primary"
disabled={disabled}>
<VideoClipIcon
size="lg"
style={disabled && t.atoms.text_contrast_low}
/>
</Button>
</>
)
}
+15 -2
View File
@@ -1,9 +1,10 @@
import React from 'react' import React from 'react'
import {View} from 'react-native' 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 {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 {clamp} from '#/lib/numbers'
import {useAutoplayDisabled} from '#/state/preferences' import {useAutoplayDisabled} from '#/state/preferences'
import {ExternalEmbedRemoveBtn} from '#/view/com/composer/ExternalEmbedRemoveBtn' import {ExternalEmbedRemoveBtn} from '#/view/com/composer/ExternalEmbedRemoveBtn'
@@ -48,6 +49,16 @@ export function VideoPreview({
<VideoTranscodeBackdrop uri={asset.uri} /> <VideoTranscodeBackdrop uri={asset.uri} />
</View> </View>
{isActivePost && ( {isActivePost && (
<>
{video.mimeType === 'image/gif' ? (
<Image
style={[a.flex_1]}
autoplay={!autoplayDisabled}
source={{uri: video.uri}}
accessibilityIgnoresInvertColors
cachePolicy="none"
/>
) : (
<BlueskyVideoView <BlueskyVideoView
url={video.uri} url={video.uri}
autoplay={!autoplayDisabled} autoplay={!autoplayDisabled}
@@ -56,6 +67,8 @@ export function VideoPreview({
ref={playerRef} ref={playerRef}
/> />
)} )}
</>
)}
<ExternalEmbedRemoveBtn onRemove={clear} /> <ExternalEmbedRemoveBtn onRemove={clear} />
{autoplayDisabled && ( {autoplayDisabled && (
<View style={[a.absolute, a.inset_0, a.justify_center, a.align_center]}> <View style={[a.absolute, a.inset_0, a.justify_center, a.align_center]}>
+1
View File
@@ -76,6 +76,7 @@ function LightboxInner({
const onKeyDown = useCallback( const onKeyDown = useCallback(
(e: KeyboardEvent) => { (e: KeyboardEvent) => {
if (e.key === 'Escape') { if (e.key === 'Escape') {
e.preventDefault()
onClose() onClose()
} else if (e.key === 'ArrowLeft') { } else if (e.key === 'ArrowLeft') {
onPressLeft() onPressLeft()
+10 -5
View File
@@ -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" resolved "https://registry.yarnpkg.com/expo-image-loader/-/expo-image-loader-5.1.0.tgz#f7d65f9b9a9714eaaf5d50a406cb34cb25262153"
integrity sha512-sEBx3zDQIODWbB5JwzE7ZL5FJD+DK3LVLWBVJy6VzsqIA6nDEnSFnsnWyCfCTSvbGigMATs1lgkC2nz3Jpve1Q== 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: expo-image-manipulator@~13.1.7:
version "13.1.7" version "13.1.7"
resolved "https://registry.yarnpkg.com/expo-image-manipulator/-/expo-image-manipulator-13.1.7.tgz#e891ce9b49d75962eafdf5b7d670116583379e76" 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: dependencies:
expo-image-loader "~5.1.0" expo-image-loader "~5.1.0"
expo-image-picker@~16.1.4: expo-image-picker@^17.0.2:
version "16.1.4" version "17.0.2"
resolved "https://registry.yarnpkg.com/expo-image-picker/-/expo-image-picker-16.1.4.tgz#d4ac2d1f64f6ec9347c3f64f8435b40e6e4dcc40" resolved "https://registry.yarnpkg.com/expo-image-picker/-/expo-image-picker-17.0.2.tgz#79af7192b2947e54686d0ece6ccbb5f6a178a809"
integrity sha512-bTmmxtw1AohUT+HxEBn2vYwdeOrj1CLpMXKjvi9FKSoSbpcarT4xxI0z7YyGwDGHbrJqyyic3I9TTdP2J2b4YA== integrity sha512-O74FIrc37KB4ZxC/BMUL3fEZwdmIB60As0q5XczRlzPvWismBl7GG3pPy+o5SGUI2jcepTvQAa2PcNcMbUZNYg==
dependencies: dependencies:
expo-image-loader "~5.1.0" expo-image-loader "~6.0.0"
expo-image@^2.4.0: expo-image@^2.4.0:
version "2.4.0" version "2.4.0"