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
/ios/Pods/
/vendor/bundle/
Gemfile.lock
# Testing
coverage/
+1 -1
View File
@@ -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",
-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 progressPercent = (progress / duration) * 100
if (duration < 3) return null
return (
<View
testID="scrubber"
@@ -373,13 +373,15 @@ export function Controls({
onPress={onPressPlayPause}
/>
<View style={a.flex_1} />
<Text
style={[
a.px_xs,
{color: t.palette.white, fontVariant: ['tabular-nums']},
]}>
{formatTime(currentTime)} / {formatTime(duration)}
</Text>
{Math.round(duration) > 0 && (
<Text
style={[
a.px_xs,
{color: t.palette.white, fontVariant: ['tabular-nums']},
]}>
{formatTime(currentTime)} / {formatTime(duration)}
</Text>
)}
{hasSubtitleTrack && (
<ControlButton
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_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 SUPPORTED_MIME_TYPES = [
+2 -2
View File
@@ -4,7 +4,6 @@ import {impactAsync, ImpactFeedbackStyle} from 'expo-haptics'
import {isIOS, isWeb} from '#/platform/detection'
import {useHapticsDisabled} from '#/state/preferences/disable-haptics'
import * as Toast from '#/view/com/util/Toast'
export function useHaptics() {
const isHapticsDisabled = useHapticsDisabled()
@@ -23,7 +22,8 @@ export function useHaptics() {
// DEV ONLY - show a toast when a haptic is meant to fire on simulator
if (__DEV__ && !Device.isDevice) {
Toast.show(`Buzzz!`)
// disabled because it's annoying
// Toast.show(`Buzzz!`)
}
},
[isHapticsDisabled],
+1 -5
View File
@@ -17,16 +17,12 @@ export async function openPicker(opts?: ImagePickerOptions) {
exif: false,
mediaTypes: ['images'],
quality: 1,
selectionLimit: 1,
...opts,
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 ?? [])
.slice(0, 4)
.filter(asset => {
if (asset.mimeType?.startsWith('image/')) return true
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 {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
+140 -120
View File
@@ -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</0>."
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
@@ -1904,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 ""
@@ -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 ""
@@ -3206,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 ""
@@ -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 ""
@@ -4236,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 ""
@@ -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 ""
@@ -5446,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 ""
@@ -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 ""
@@ -6443,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 ""
@@ -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</0>."
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}</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</0>."
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 ""
+19
View File
@@ -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 (
<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
style={[a.text_md, a.font_bold, a.pb_md, t.atoms.text_contrast_high]}>
<Trans>Moderation tools</Trans>
+9 -2
View File
@@ -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
}
+2 -2
View File
@@ -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
/**
+175 -93
View File
@@ -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 (
<BottomSheetPortalProvider>
<KeyboardAvoidingView
testID="composePostView"
behavior={isIOS ? 'padding' : 'height'}
keyboardVerticalOffset={keyboardVerticalOffset}
style={a.flex_1}>
<View
style={[a.flex_1, viewStyles]}
aria-modal
accessibilityViewIsModal>
<ComposerTopBar
canPost={canPost}
isReply={!!replyTo}
isPublishQueued={publishOnUpload}
isPublishing={isPublishing}
isThread={thread.posts.length > 1}
publishingStage={publishingStage}
topBarAnimatedStyle={topBarAnimatedStyle}
onCancel={onPressCancel}
onPublish={onPressPublish}>
{missingAltError && <AltTextReminder error={missingAltError} />}
<ErrorBanner
error={error}
videoState={erroredVideo}
clearError={() => setError('')}
clearVideo={
erroredVideoPostId
? () => clearVideo(erroredVideoPostId)
: () => {}
}
/>
</ComposerTopBar>
<Animated.ScrollView
ref={scrollViewRef}
layout={native(LinearTransition)}
onScroll={scrollHandler}
contentContainerStyle={a.flex_grow}
style={a.flex_1}
keyboardShouldPersistTaps="always"
onContentSizeChange={onScrollViewContentSizeChange}
onLayout={onScrollViewLayout}>
{replyTo ? <ComposerReplyTo replyTo={replyTo} /> : undefined}
{thread.posts.map((post, index) => (
<React.Fragment key={post.id}>
<ComposerPost
post={post}
dispatch={composerDispatch}
textInput={post.id === activePost.id ? textInput : null}
isFirstPost={index === 0}
isLastPost={index === thread.posts.length - 1}
isPartOfThread={thread.posts.length > 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}
<RootSiblingParent>
<KeyboardAvoidingView
testID="composePostView"
behavior={isIOS ? 'padding' : 'height'}
keyboardVerticalOffset={keyboardVerticalOffset}
style={a.flex_1}>
<View
style={[a.flex_1, viewStyles]}
aria-modal
accessibilityViewIsModal>
<RootSiblingParent>
<ComposerTopBar
canPost={canPost}
isReply={!!replyTo}
isPublishQueued={publishOnUpload}
isPublishing={isPublishing}
isThread={thread.posts.length > 1}
publishingStage={publishingStage}
topBarAnimatedStyle={topBarAnimatedStyle}
onCancel={onPressCancel}
onPublish={onPressPublish}>
{missingAltError && <AltTextReminder error={missingAltError} />}
<ErrorBanner
error={error}
videoState={erroredVideo}
clearError={() => setError('')}
clearVideo={
erroredVideoPostId
? () => clearVideo(erroredVideoPostId)
: () => {}
}
/>
{isWebFooterSticky && post.id === activePost.id && (
<View style={styles.stickyFooterWeb}>{footer}</View>
)}
</React.Fragment>
))}
</Animated.ScrollView>
{!isWebFooterSticky && footer}
</View>
</ComposerTopBar>
<Prompt.Basic
control={discardPromptControl}
title={_(msg`Discard draft?`)}
description={_(msg`Are you sure you'd like to discard this draft?`)}
onConfirm={onClose}
confirmButtonCta={_(msg`Discard`)}
confirmButtonColor="negative"
/>
</KeyboardAvoidingView>
<Animated.ScrollView
ref={scrollViewRef}
layout={native(LinearTransition)}
onScroll={scrollHandler}
contentContainerStyle={a.flex_grow}
style={a.flex_1}
keyboardShouldPersistTaps="always"
onContentSizeChange={onScrollViewContentSizeChange}
onLayout={onScrollViewLayout}>
{replyTo ? <ComposerReplyTo replyTo={replyTo} /> : undefined}
{thread.posts.map((post, index) => (
<React.Fragment key={post.id}>
<ComposerPost
post={post}
dispatch={composerDispatch}
textInput={post.id === activePost.id ? textInput : null}
isFirstPost={index === 0}
isLastPost={index === thread.posts.length - 1}
isPartOfThread={thread.posts.length > 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 && (
<View style={styles.stickyFooterWeb}>{footer}</View>
)}
</React.Fragment>
))}
</Animated.ScrollView>
{!isWebFooterSticky && footer}
</RootSiblingParent>
</View>
<Prompt.Basic
control={discardPromptControl}
title={_(msg`Discard draft?`)}
description={_(msg`Are you sure you'd like to discard this draft?`)}
onConfirm={onClose}
confirmButtonCta={_(msg`Discard`)}
confirmButtonColor="negative"
/>
</KeyboardAvoidingView>
</RootSiblingParent>
</BottomSheetPortalProvider>
)
}
@@ -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<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 (
<View
style={[
@@ -1307,15 +1393,11 @@ function ComposerFooter({
<VideoUploadToolbar state={video} />
) : (
<ToolbarWrapper style={[a.flex_row, a.align_center, a.gap_xs]}>
<SelectPhotoBtn
size={images.length}
disabled={media?.type === 'images' ? isMaxImages : !!media}
onAdd={onImageAdd}
/>
<SelectVideoBtn
onSelectVideo={asset => onSelectVideo(post.id, asset)}
disabled={!!media}
setError={onError}
<SelectMediaButton
disabled={isMediaSelectionDisabled}
allowedAssetTypes={selectedAssetsType}
selectedAssetsCount={selectedAssetsCount}
onSelectAssets={onSelectAssets}
/>
<OpenCameraBtn
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]}>
<Image
style={imageStyle}
source={{
uri: (image.transformed ?? image.source).path,
}}
source={{uri: (image.transformed ?? image.source).path}}
contentFit="contain"
accessible={true}
accessibilityIgnoresInvertColors
enableLiveTextInteraction
autoplay={false}
/>
</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>
</>
)
}
+22 -9
View File
@@ -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({
<VideoTranscodeBackdrop uri={asset.uri} />
</View>
{isActivePost && (
<BlueskyVideoView
url={video.uri}
autoplay={!autoplayDisabled}
beginMuted={true}
forceTakeover={true}
ref={playerRef}
/>
<>
{video.mimeType === 'image/gif' ? (
<Image
style={[a.flex_1]}
autoplay={!autoplayDisabled}
source={{uri: video.uri}}
accessibilityIgnoresInvertColors
cachePolicy="none"
/>
) : (
<BlueskyVideoView
url={video.uri}
autoplay={!autoplayDisabled}
beginMuted={true}
forceTakeover={true}
ref={playerRef}
/>
)}
</>
)}
<ExternalEmbedRemoveBtn onRemove={clear} />
{autoplayDisabled && (
+1
View File
@@ -76,6 +76,7 @@ function LightboxInner({
const onKeyDown = useCallback(
(e: KeyboardEvent) => {
if (e.key === 'Escape') {
e.preventDefault()
onClose()
} else if (e.key === 'ArrowLeft') {
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"
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"