Merge branch 'main' into app-1788

This commit is contained in:
vineyardbovines
2026-02-05 08:59:22 -05:00
64 changed files with 29009 additions and 22510 deletions
+4 -4
View File
@@ -313,22 +313,22 @@ module.exports = function (_config) {
{
ios: {
enableFullScreenImage_legacy: true, // iOS only
backgroundColor: '#A8CCFF', // primary_200
backgroundColor: '#006AFF', // primary_500
image: './assets/splash/splash.png',
resizeMode: 'cover',
dark: {
enableFullScreenImage_legacy: true, // iOS only
backgroundColor: '#00398A', // primary_800
backgroundColor: '#002861', // primary_900
image: './assets/splash/splash-dark.png',
resizeMode: 'cover',
},
},
android: {
backgroundColor: '#A8CCFF', // primary_200
backgroundColor: '#006AFF', // primary_500
image: './assets/splash/android-splash-logo-white.png',
imageWidth: 102, // even division of 306px
dark: {
backgroundColor: '#00398A', // primary_800
backgroundColor: '#002861', // primary_900
image: './assets/splash/android-splash-logo-white.png',
imageWidth: 102,
},
Binary file not shown.

After

Width:  |  Height:  |  Size: 153 KiB

+26 -20
View File
@@ -50,7 +50,11 @@ export function Embed({
} else if (e.type === 'video') {
return (
<Outer style={style}>
<VideoItem thumbnail={e.view.thumbnail} alt={e.view.alt} />
{e.view.presentation === 'gif' ? (
<GifItem thumbnail={e.view.thumbnail} alt={e.view.alt} />
) : (
<VideoItem thumbnail={e.view.thumbnail} alt={e.view.alt} />
)}
</Outer>
)
} else if (
@@ -81,11 +85,29 @@ export function ImageItem({
alt,
children,
}: {
thumbnail: string
thumbnail?: string
alt?: string
children?: React.ReactNode
}) {
const t = useTheme()
if (!thumbnail) {
return (
<View
style={[
{backgroundColor: 'black'},
a.flex_1,
a.aspect_square,
{maxWidth: 100},
a.rounded_xs,
]}
accessibilityLabel={alt}
accessibilityHint="">
{children}
</View>
)
}
return (
<View style={[a.relative, a.flex_1, a.aspect_square, {maxWidth: 100}]}>
<Image
@@ -103,7 +125,7 @@ export function ImageItem({
)
}
export function GifItem({thumbnail, alt}: {thumbnail: string; alt?: string}) {
export function GifItem({thumbnail, alt}: {thumbnail?: string; alt?: string}) {
return (
<ImageItem thumbnail={thumbnail} alt={alt}>
<View style={[a.absolute, a.inset_0, a.justify_center, a.align_center]}>
@@ -125,22 +147,6 @@ export function VideoItem({
thumbnail?: string
alt?: string
}) {
if (!thumbnail) {
return (
<View
style={[
{backgroundColor: 'black'},
a.flex_1,
a.aspect_square,
{maxWidth: 100},
a.justify_center,
a.align_center,
a.rounded_xs,
]}>
<PlayButtonIcon size={24} />
</View>
)
}
return (
<ImageItem thumbnail={thumbnail} alt={alt}>
<View style={[a.absolute, a.inset_0, a.justify_center, a.align_center]}>
@@ -157,7 +163,7 @@ const styles = StyleSheet.create({
paddingHorizontal: 6,
paddingVertical: 3,
position: 'absolute',
right: 5,
left: 5,
bottom: 5,
zIndex: 2,
},
+8 -128
View File
@@ -1,76 +1,17 @@
import {useRef, useState} from 'react'
import {
Pressable,
type StyleProp,
StyleSheet,
TouchableOpacity,
View,
type ViewStyle,
} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {type StyleProp, View, type ViewStyle} from 'react-native'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {HITSLOP_20} from '#/lib/constants'
import {clamp} from '#/lib/numbers'
import {type EmbedPlayerParams} from '#/lib/strings/embed-player'
import {useAutoplayDisabled} from '#/state/preferences'
import {useLargeAltBadgeEnabled} from '#/state/preferences/large-alt-badge'
import {atoms as a, useTheme} from '#/alf'
import {Fill} from '#/components/Fill'
import {Loader} from '#/components/Loader'
import * as Prompt from '#/components/Prompt'
import {Text} from '#/components/Typography'
import {PlayButtonIcon} from '#/components/video/PlayButtonIcon'
import {IS_WEB} from '#/env'
import {MediaInsetBorder} from '#/components/MediaInsetBorder'
import {GifView} from '../../../../../modules/expo-bluesky-gif-view'
import {type GifViewStateChangeEvent} from '../../../../../modules/expo-bluesky-gif-view/src/GifView.types'
function PlaybackControls({
onPress,
isPlaying,
isLoaded,
}: {
onPress: () => void
isPlaying: boolean
isLoaded: boolean
}) {
const {_} = useLingui()
const t = useTheme()
return (
<Pressable
accessibilityRole="button"
accessibilityHint={_(msg`Plays or pauses the GIF`)}
accessibilityLabel={isPlaying ? _(msg`Pause`) : _(msg`Play`)}
style={[
a.absolute,
a.align_center,
a.justify_center,
!isLoaded && a.border,
t.atoms.border_contrast_medium,
a.inset_0,
a.w_full,
a.h_full,
{
zIndex: 2,
backgroundColor: !isLoaded
? t.atoms.bg_contrast_25.backgroundColor
: undefined,
},
]}
onPress={onPress}>
{!isLoaded ? (
<View>
<View style={[a.align_center, a.justify_center]}>
<Loader size="xl" />
</View>
</View>
) : !isPlaying ? (
<PlayButtonIcon />
) : undefined}
</Pressable>
)
}
import {GifPresentationControls} from '../VideoEmbed/GifPresentationControls'
export function GifEmbed({
params,
@@ -120,8 +61,6 @@ export function GifEmbed({
style={[
a.rounded_md,
a.overflow_hidden,
a.border,
t.atoms.border_contrast_low,
{backgroundColor: t.palette.black},
{aspectRatio},
style,
@@ -139,10 +78,12 @@ export function GifEmbed({
right: -2,
},
]}>
<PlaybackControls
<MediaInsetBorder />
<GifPresentationControls
onPress={onPress}
isPlaying={playerState.isPlaying}
isLoaded={playerState.isLoaded}
isLoading={!playerState.isLoaded}
altText={!hideAlt && isPreferredAltText ? altText : undefined}
/>
<GifView
source={params.playerUri}
@@ -164,68 +105,7 @@ export function GifEmbed({
]}
/>
)}
{!hideAlt && isPreferredAltText && <AltText text={altText} />}
</View>
</View>
)
}
function AltText({text}: {text: string}) {
const control = Prompt.usePromptControl()
const largeAltBadge = useLargeAltBadgeEnabled()
const {_} = useLingui()
return (
<>
<TouchableOpacity
testID="altTextButton"
accessibilityRole="button"
accessibilityLabel={_(msg`Show alt text`)}
accessibilityHint=""
hitSlop={HITSLOP_20}
onPress={control.open}
style={styles.altContainer}>
<Text
style={[styles.alt, largeAltBadge && a.text_xs]}
accessible={false}>
<Trans>ALT</Trans>
</Text>
</TouchableOpacity>
<Prompt.Outer control={control}>
<Prompt.Content>
<Prompt.TitleText>
<Trans>Alt Text</Trans>
</Prompt.TitleText>
<Prompt.DescriptionText selectable>{text}</Prompt.DescriptionText>
</Prompt.Content>
<Prompt.Actions>
<Prompt.Action
onPress={() => control.close()}
cta={_(msg`Close`)}
color="secondary"
/>
</Prompt.Actions>
</Prompt.Outer>
</>
)
}
const styles = StyleSheet.create({
altContainer: {
backgroundColor: 'rgba(0, 0, 0, 0.75)',
borderRadius: 6,
paddingHorizontal: IS_WEB ? 8 : 6,
paddingVertical: IS_WEB ? 6 : 3,
position: 'absolute',
// Related to margin/gap hack. This keeps the alt label in the same position
// on all platforms
right: IS_WEB ? 8 : 5,
bottom: IS_WEB ? 8 : 5,
zIndex: 2,
},
alt: {
color: 'white',
fontSize: IS_WEB ? 10 : 7,
fontWeight: '600',
},
})
@@ -0,0 +1,132 @@
import {Pressable, StyleSheet, TouchableOpacity, View} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {HITSLOP_20} from '#/lib/constants'
import {atoms as a, useTheme} from '#/alf'
import {Fill} from '#/components/Fill'
import {Loader} from '#/components/Loader'
import * as Prompt from '#/components/Prompt'
import {Text} from '#/components/Typography'
import {PlayButtonIcon} from '#/components/video/PlayButtonIcon'
export function GifPresentationControls({
onPress,
isPlaying,
isLoading,
altText,
}: {
onPress: () => void
isPlaying: boolean
isLoading?: boolean
altText?: string
}) {
const {_} = useLingui()
const t = useTheme()
return (
<>
<Pressable
accessibilityRole="button"
accessibilityHint={_(msg`Plays or pauses the GIF`)}
accessibilityLabel={isPlaying ? _(msg`Pause`) : _(msg`Play`)}
style={[
a.absolute,
a.align_center,
a.justify_center,
a.inset_0,
a.w_full,
a.h_full,
{zIndex: 2},
]}
onPress={onPress}>
{isLoading ? (
<View style={[a.align_center, a.justify_center]}>
<Loader size="xl" />
</View>
) : !isPlaying ? (
<PlayButtonIcon />
) : undefined}
</Pressable>
{!isPlaying && (
<Fill
style={[
t.name === 'light' ? t.atoms.bg_contrast_975 : t.atoms.bg,
{
opacity: 0.2,
zIndex: 1,
},
]}
/>
)}
<View style={styles.gifBadgeContainer}>
<Text style={[{color: 'white'}, a.font_bold, a.text_xs]}>
<Trans>GIF</Trans>
</Text>
</View>
{altText && <AltBadge text={altText} />}
</>
)
}
function AltBadge({text}: {text: string}) {
const control = Prompt.usePromptControl()
const {_} = useLingui()
return (
<>
<TouchableOpacity
testID="altTextButton"
accessibilityRole="button"
accessibilityLabel={_(msg`Show alt text`)}
accessibilityHint=""
hitSlop={HITSLOP_20}
onPress={control.open}
style={styles.altBadgeContainer}>
<Text
style={[{color: 'white'}, a.font_bold, a.text_xs]}
accessible={false}>
<Trans>ALT</Trans>
</Text>
</TouchableOpacity>
<Prompt.Outer control={control}>
<Prompt.Content>
<Prompt.TitleText>
<Trans>Alt Text</Trans>
</Prompt.TitleText>
<Prompt.DescriptionText selectable>{text}</Prompt.DescriptionText>
</Prompt.Content>
<Prompt.Actions>
<Prompt.Action
onPress={() => control.close()}
cta={_(msg`Close`)}
color="secondary"
/>
</Prompt.Actions>
</Prompt.Outer>
</>
)
}
const styles = StyleSheet.create({
gifBadgeContainer: {
backgroundColor: 'rgba(0, 0, 0, 0.75)',
borderRadius: 6,
paddingHorizontal: 4,
paddingVertical: 3,
position: 'absolute',
left: 6,
bottom: 6,
zIndex: 2,
},
altBadgeContainer: {
backgroundColor: 'rgba(0, 0, 0, 0.75)',
borderRadius: 6,
paddingHorizontal: 4,
paddingVertical: 3,
position: 'absolute',
right: 6,
bottom: 6,
zIndex: 2,
},
})
@@ -15,6 +15,7 @@ import {Play_Filled_Corner0_Rounded as PlayIcon} from '#/components/icons/Play'
import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as UnmuteIcon} from '#/components/icons/Speaker'
import {MediaInsetBorder} from '#/components/MediaInsetBorder'
import {useVideoMuteState} from '#/components/Post/Embed/VideoEmbed/VideoVolumeContext'
import {GifPresentationControls} from '../GifPresentationControls'
import {TimeIndicator} from './TimeIndicator'
export function VideoEmbedInnerNative({
@@ -50,12 +51,14 @@ export function VideoEmbedInnerNative({
throw new Error(error)
}
const isGif = embed.presentation === 'gif'
return (
<View style={[a.flex_1, a.relative]}>
<BlueskyVideoView
url={embed.playlist}
autoplay={!autoplayDisabled && !isWithinMessage}
beginMuted={autoplayDisabled ? false : muted}
beginMuted={isGif || autoplayDisabled ? false : muted}
style={[a.rounded_sm]}
onActiveChange={e => {
setIsActive(e.nativeEvent.isActive)
@@ -82,25 +85,36 @@ export function VideoEmbedInnerNative({
}
accessibilityHint=""
/>
<VideoControls
enterFullscreen={() => {
videoRef.current?.enterFullscreen(true)
}}
toggleMuted={() => {
videoRef.current?.toggleMuted()
}}
togglePlayback={() => {
videoRef.current?.togglePlayback()
}}
isPlaying={isPlaying}
timeRemaining={timeRemaining}
/>
{isGif ? (
<GifPresentationControls
onPress={() => {
videoRef.current?.togglePlayback()
}}
isPlaying={isPlaying}
isLoading={false}
altText={embed.alt}
/>
) : (
<VideoPresentationControls
enterFullscreen={() => {
videoRef.current?.enterFullscreen(true)
}}
toggleMuted={() => {
videoRef.current?.toggleMuted()
}}
togglePlayback={() => {
videoRef.current?.togglePlayback()
}}
isPlaying={isPlaying}
timeRemaining={timeRemaining}
/>
)}
<MediaInsetBorder />
</View>
)
}
function VideoControls({
function VideoPresentationControls({
enterFullscreen,
toggleMuted,
togglePlayback,
@@ -21,7 +21,7 @@ export function VideoEmbedInnerWeb({
active: boolean
setActive: () => void
onScreen: boolean
lastKnownTime: React.MutableRefObject<number | undefined>
lastKnownTime: React.RefObject<number | undefined>
}) {
const containerRef = useRef<HTMLDivElement>(null)
const videoRef = useRef<HTMLVideoElement>(null)
@@ -37,7 +37,7 @@ export function VideoEmbedInnerWeb({
throw error
}
const hlsRef = useHLS({
const {hlsRef, loop} = useHLS({
playlist: embed.playlist,
setHasSubtitleTrack,
setError,
@@ -64,11 +64,12 @@ export function VideoEmbedInnerWeb({
style={{width: '100%', height: '100%', objectFit: 'contain'}}
playsInline
preload="none"
muted={!focused}
muted={embed.presentation === 'gif' || !focused}
aria-labelledby={embed.alt ? figId : undefined}
onTimeUpdate={e => {
lastKnownTime.current = e.currentTarget.currentTime
}}
loop={loop}
/>
{embed.alt && (
<figcaption
@@ -99,6 +100,8 @@ export function VideoEmbedInnerWeb({
onScreen={onScreen}
fullscreenRef={containerRef}
hasSubtitleTrack={hasSubtitleTrack}
isGif={embed.presentation === 'gif'}
altText={embed.alt}
/>
</div>
</View>
@@ -192,29 +195,6 @@ function useHLS({
},
)
const flushOnLoop = useNonReactiveCallback(() => {
if (!Hls) return
if (!hlsRef.current) return
const hls = hlsRef.current
// the above callback will catch most stale frags, but there's a corner case -
// if there's only one segment in the video, it won't get flushed because it avoids
// flushing the currently active segment. Therefore, we have to catch it when we loop
if (
hls.nextAutoLevel > 0 &&
lowQualityFragments.length === 1 &&
lowQualityFragments[0].start === 0
) {
const lowQualFrag = lowQualityFragments[0]
hls.trigger(Hls.Events.BUFFER_FLUSHING, {
startOffset: lowQualFrag.start,
endOffset: lowQualFrag.end,
type: 'video',
})
setLowQualityFragments([])
}
})
useEffect(() => {
if (!videoRef.current) return
if (!Hls) return
@@ -242,20 +222,6 @@ function useHLS({
hls.attachMedia(videoRef.current)
hls.loadSource(playlist)
// manually loop, so if we've flushed the first buffer it doesn't get confused
const abortController = new AbortController()
const {signal} = abortController
const videoNode = videoRef.current
videoNode.addEventListener(
'ended',
() => {
flushOnLoop()
videoNode.currentTime = 0
videoNode.play()
},
{signal},
)
hls.on(Hls.Events.FRAG_LOADED, () => {
BandwidthEstimate.set(hls.bandwidthEstimate)
})
@@ -293,17 +259,65 @@ function useHLS({
hlsRef.current = undefined
hls.detachMedia()
hls.destroy()
}
}, [playlist, setError, setHasSubtitleTrack, videoRef, handleFragChange, Hls])
const flushOnLoop = useNonReactiveCallback(() => {
if (!Hls) return
if (!hlsRef.current) return
const hls = hlsRef.current
// `handleFragChange` will catch most stale frags, but there's a corner case -
// if there's only one segment in the video, it won't get flushed because it avoids
// flushing the currently active segment. Therefore, we have to catch it when we loop
if (
hls.nextAutoLevel > 0 &&
lowQualityFragments.length === 1 &&
lowQualityFragments[0].start === 0
) {
const lowQualFrag = lowQualityFragments[0]
hls.trigger(Hls.Events.BUFFER_FLUSHING, {
startOffset: lowQualFrag.start,
endOffset: lowQualFrag.end,
type: 'video',
})
setLowQualityFragments([])
}
})
// manually loop, so if we've flushed the first buffer it doesn't get confused
const hasLowQualityFragmentAtStart = lowQualityFragments.some(
frag => frag.start === 0,
)
useEffect(() => {
if (!videoRef.current) return
// use `loop` prop on `<video>` element if the starting frag is high quality.
// otherwise, we need to do it with an event listener as we may need to manually flush the frag
if (!hasLowQualityFragmentAtStart) return
const abortController = new AbortController()
const {signal} = abortController
const videoNode = videoRef.current
videoNode.addEventListener(
'ended',
() => {
flushOnLoop()
videoNode.currentTime = 0
const maybePromise = videoNode.play() as Promise<void> | undefined
if (maybePromise) {
maybePromise.catch(() => {})
}
},
{signal},
)
return () => {
abortController.abort()
}
}, [
playlist,
setError,
setHasSubtitleTrack,
videoRef,
handleFragChange,
flushOnLoop,
Hls,
])
}, [videoRef, flushOnLoop, hasLowQualityFragmentAtStart])
return hlsRef
return {
hlsRef,
loop: !hasLowQualityFragmentAtStart,
}
}
@@ -27,6 +27,7 @@ import {Play_Filled_Corner0_Rounded as PlayIcon} from '#/components/icons/Play'
import {Loader} from '#/components/Loader'
import {Text} from '#/components/Typography'
import {IS_WEB_MOBILE_IOS, IS_WEB_TOUCH_DEVICE} from '#/env'
import {GifPresentationControls} from '../../GifPresentationControls'
import {TimeIndicator} from '../TimeIndicator'
import {ControlButton} from './ControlButton'
import {Scrubber} from './Scrubber'
@@ -44,6 +45,8 @@ export function Controls({
fullscreenRef,
hlsLoading,
hasSubtitleTrack,
isGif,
altText,
}: {
videoRef: React.RefObject<HTMLVideoElement | null>
hlsRef: React.RefObject<Hls | undefined | null>
@@ -55,6 +58,8 @@ export function Controls({
fullscreenRef: React.RefObject<HTMLDivElement | null>
hlsLoading: boolean
hasSubtitleTrack: boolean
isGif: boolean
altText?: string
}) {
const {
play,
@@ -125,13 +130,14 @@ export function Controls({
const autoplayDisabled = useAutoplayDisabled() || isWithinMessage
useEffect(() => {
if (active) {
if (onScreen) {
// GIFs play immediately, videos wait until onScreen
if (onScreen || isGif) {
if (!autoplayDisabled) play()
} else {
pause()
}
}
}, [onScreen, pause, active, play, autoplayDisabled])
}, [onScreen, pause, active, play, autoplayDisabled, isGif])
// use minimal quality when not focused
useEffect(() => {
@@ -287,6 +293,17 @@ export function Controls({
((focused || autoplayDisabled) && !playing) ||
(interactingViaKeypress ? hasFocus : hovered)
if (isGif) {
return (
<GifPresentationControls
isPlaying={playing}
isLoading={showSpinner}
onPress={onPressPlayPause}
altText={altText}
/>
)
}
return (
<div
style={{
@@ -26,15 +26,25 @@ import {IS_WEB_FIREFOX} from '#/env'
import {useActiveVideoWeb} from './ActiveVideoWebContext'
import * as VideoFallback from './VideoEmbedInner/VideoFallback'
const noop = () => {}
export function VideoEmbed({embed}: {embed: AppBskyEmbedVideo.View}) {
const t = useTheme()
const ref = useRef<HTMLDivElement>(null)
const {active, setActive, sendPosition, currentActiveView} =
useActiveVideoWeb()
const {
active: activeFromContext,
setActive,
sendPosition,
currentActiveView,
} = useActiveVideoWeb()
const [onScreen, setOnScreen] = useState(false)
const [isFullscreen] = useFullscreen()
const lastKnownTime = useRef<number | undefined>(undefined)
const isGif = embed.presentation === 'gif'
// GIFs don't participate in the "one video at a time" system
const active = isGif || activeFromContext
useEffect(() => {
if (!ref.current) return
if (isFullscreen && !IS_WEB_FIREFOX) return
@@ -43,15 +53,18 @@ export function VideoEmbed({embed}: {embed: AppBskyEmbedVideo.View}) {
const entry = entries[0]
if (!entry) return
setOnScreen(entry.isIntersecting)
sendPosition(
entry.boundingClientRect.y + entry.boundingClientRect.height / 2,
)
// GIFs don't send position - they don't compete to be the active video
if (!isGif) {
sendPosition(
entry.boundingClientRect.y + entry.boundingClientRect.height / 2,
)
}
},
{threshold: 0.5},
)
observer.observe(ref.current)
return () => observer.disconnect()
}, [sendPosition, isFullscreen])
}, [sendPosition, isFullscreen, isGif])
const [key, setKey] = useState(0)
const renderError = useCallback(
@@ -107,7 +120,7 @@ export function VideoEmbed({embed}: {embed: AppBskyEmbedVideo.View}) {
return (
<View style={[a.pt_xs]}>
<ViewportObserver
sendPosition={sendPosition}
sendPosition={isGif ? noop : sendPosition}
isAnyViewActive={currentActiveView !== null}>
<ConstrainedImage
fullBleed
@@ -0,0 +1,154 @@
import {useCallback} from 'react'
import {View} from 'react-native'
import {Image} from 'expo-image'
import {LinearGradient} from 'expo-linear-gradient'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {atoms as a, useTheme, web} from '#/alf'
import {Button, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
import {useNuxDialogContext} from '#/components/dialogs/nuxs'
import {Sparkle_Stroke2_Corner0_Rounded as SparkleIcon} from '#/components/icons/Sparkle'
import {Text} from '#/components/Typography'
import {IS_E2E, IS_NATIVE, IS_WEB} from '#/env'
import {createIsEnabledCheck, isExistingUserAsOf} from './utils'
export const enabled = createIsEnabledCheck(props => {
return (
!IS_E2E &&
IS_NATIVE &&
isExistingUserAsOf(
'2026-02-05T00:00:00.000Z',
props.currentProfile.createdAt,
)
)
})
export function DraftsAnnouncement() {
const t = useTheme()
const {_} = useLingui()
const nuxDialogs = useNuxDialogContext()
const control = Dialog.useDialogControl()
Dialog.useAutoOpen(control)
const onClose = useCallback(() => {
nuxDialogs.dismissActiveNux()
}, [nuxDialogs])
return (
<Dialog.Outer
control={control}
onClose={onClose}
nativeOptions={{preventExpansion: true}}>
<Dialog.Handle fill={t.palette.primary_400} />
<Dialog.ScrollableInner
label={_(msg`Introducing drafts`)}
style={[web({maxWidth: 440})]}
contentContainerStyle={[
{
paddingTop: 0,
paddingLeft: 0,
paddingRight: 0,
},
]}>
<View
style={[
a.align_center,
a.overflow_hidden,
{
paddingTop: IS_WEB ? 24 : 40,
borderTopLeftRadius: a.rounded_md.borderRadius,
borderTopRightRadius: a.rounded_md.borderRadius,
},
]}>
<LinearGradient
colors={[t.palette.primary_100, t.palette.primary_200]}
locations={[0, 1]}
start={{x: 0, y: 0}}
end={{x: 0, y: 1}}
style={[a.absolute, a.inset_0]}
/>
<View
style={[a.flex_row, a.align_center, a.gap_xs, {marginBottom: -12}]}>
<SparkleIcon fill={t.palette.primary_800} size="sm" />
<Text
style={[
a.font_semi_bold,
{
color: t.palette.primary_800,
},
]}>
<Trans>New Feature</Trans>
</Text>
</View>
<Image
accessibilityIgnoresInvertColors
source={require('../../../../assets/images/drafts_announcement_nux.webp')}
style={[
a.w_full,
{
aspectRatio: 393 / 226,
},
]}
alt={_(
msg({
message: `A screenshot of a the post composer with a new button next to the post button that says "Drafts", with a rainbow firework effect. Below, the text in the composer reads "Hey, did you hear the news? Bluesky has drafts now???".`,
comment:
'Contains a post that originally appeared in English. Consider translating the post text if it makes sense in your language, and noting that the post was translated from English.',
}),
)}
/>
</View>
<View style={[a.align_center, a.px_xl, a.pt_xl, a.gap_2xl, a.pb_sm]}>
<View style={[a.gap_sm, a.align_center]}>
<Text
style={[
a.text_3xl,
a.leading_tight,
a.font_bold,
a.text_center,
{
fontSize: IS_WEB ? 28 : 32,
maxWidth: 300,
},
]}>
<Trans>Drafts</Trans>
</Text>
<Text
style={[
a.text_md,
a.leading_snug,
a.text_center,
{
maxWidth: 340,
},
]}>
<Trans>
Not ready to hit post? Keep your best ideas in Drafts until the
timing is just right.
</Trans>
</Text>
</View>
{!IS_WEB && (
<Button
label={_(msg`Close`)}
size="large"
color="primary"
onPress={() => control.close()}
style={[a.w_full]}>
<ButtonText>
<Trans>Finally!</Trans>
</ButtonText>
</Button>
)}
</View>
<Dialog.Close />
</Dialog.ScrollableInner>
</Dialog.Outer>
)
}
+6 -6
View File
@@ -19,9 +19,9 @@ import {useProfileQuery} from '#/state/queries/profile'
import {type SessionAccount, useSession} from '#/state/session'
import {useOnboardingState} from '#/state/shell'
import {
enabled as isLiveNowBetaDialogEnabled,
LiveNowBetaDialog,
} from '#/components/dialogs/nuxs/LiveNowBetaDialog'
DraftsAnnouncement,
enabled as isDraftsAnnouncementEnabled,
} from '#/components/dialogs/nuxs/DraftsAnnouncement'
import {isSnoozed, snooze, unsnooze} from '#/components/dialogs/nuxs/snoozing'
import {type EnabledCheckProps} from '#/components/dialogs/nuxs/utils'
import {useAnalytics} from '#/analytics'
@@ -37,8 +37,8 @@ const queuedNuxs: {
enabled?: (props: EnabledCheckProps) => boolean
}[] = [
{
id: Nux.LiveNowBetaDialog,
enabled: isLiveNowBetaDialogEnabled,
id: Nux.DraftsAnnouncement,
enabled: isDraftsAnnouncementEnabled,
},
]
@@ -186,7 +186,7 @@ function Inner({
return (
<Context.Provider value={ctx}>
{/*For example, activeNux === Nux.NeueTypography && <NeueTypography />*/}
{activeNux === Nux.LiveNowBetaDialog && <LiveNowBetaDialog />}
{activeNux === Nux.DraftsAnnouncement && <DraftsAnnouncement />}
</Context.Provider>
)
}
+2
View File
@@ -354,6 +354,8 @@ async function resolveMedia(
alt: videoDraft.altText || undefined,
captions: captions.length === 0 ? undefined : captions,
aspectRatio,
presentation:
videoDraft.video.mimeType === 'image/gif' ? 'gif' : 'default',
}
}
if (embedDraft.media?.type === 'gif') {
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+95 -54
View File
@@ -579,6 +579,11 @@ msgstr ""
msgid "A screenshot of a profile page with a bell icon next to the follow button, indicating the new activity notifications feature."
msgstr ""
#. Contains a post that originally appeared in English. Consider translating the post text if it makes sense in your language, and noting that the post was translated from English.
#: src/components/dialogs/nuxs/DraftsAnnouncement.tsx:97
msgid "A screenshot of a the post composer with a new button next to the post button that says \"Drafts\", with a rainbow firework effect. Below, the text in the composer reads \"Hey, did you hear the news? Bluesky has drafts now???\"."
msgstr ""
#: src/Navigation.tsx:535
#: src/screens/Settings/AboutSettings.tsx:73
#: src/screens/Settings/Settings.tsx:260
@@ -780,7 +785,7 @@ msgid "Add image"
msgstr ""
#. Accessibility label for button in composer to add images, a video, or a GIF to a post
#: src/view/com/composer/SelectMediaButton.tsx:481
#: src/view/com/composer/SelectMediaButton.tsx:499
msgid "Add media to post"
msgstr ""
@@ -1016,7 +1021,7 @@ msgstr ""
msgid "Already signed in as @{0}"
msgstr ""
#: src/components/Post/Embed/ExternalEmbed/Gif.tsx:191
#: src/components/Post/Embed/VideoEmbed/GifPresentationControls.tsx:89
#: src/view/com/composer/GifAltText.tsx:100
#: src/view/com/composer/photos/Gallery.tsx:188
msgid "ALT"
@@ -1032,7 +1037,7 @@ msgstr ""
msgid "Alt text"
msgstr ""
#: src/components/Post/Embed/ExternalEmbed/Gif.tsx:197
#: src/components/Post/Embed/VideoEmbed/GifPresentationControls.tsx:95
msgid "Alt Text"
msgstr ""
@@ -1058,7 +1063,7 @@ msgstr ""
msgid "An error has occurred"
msgstr ""
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx:421
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx:438
msgid "An error occurred"
msgstr ""
@@ -1087,7 +1092,7 @@ msgstr ""
msgid "An error occurred while loading the video. Please try again later."
msgstr ""
#: src/components/Post/Embed/VideoEmbed/index.web.tsx:226
#: src/components/Post/Embed/VideoEmbed/index.web.tsx:239
msgid "An error occurred while loading the video. Please try again."
msgstr ""
@@ -1180,7 +1185,7 @@ msgstr ""
msgid "Animals"
msgstr ""
#: src/components/Post/Embed/ExternalEmbed/Gif.tsx:154
#: src/components/Post/Embed/ExternalEmbed/Gif.tsx:95
msgid "Animated GIF"
msgstr ""
@@ -1404,8 +1409,8 @@ msgstr ""
#: src/screens/Settings/components/ChangePasswordDialog.tsx:281
#: src/screens/Signup/BackNextButtons.tsx:41
#: src/screens/StarterPack/Wizard/index.tsx:324
#: src/view/com/composer/drafts/DraftsListDialog.tsx:79
#: src/view/com/composer/drafts/DraftsListDialog.tsx:85
#: src/view/com/composer/drafts/DraftsListDialog.tsx:80
#: src/view/com/composer/drafts/DraftsListDialog.tsx:86
msgid "Back"
msgstr ""
@@ -2093,6 +2098,7 @@ msgstr ""
#: src/components/dialogs/nuxs/ActivitySubscriptions.tsx:167
#: src/components/dialogs/nuxs/BookmarksAnnouncement.tsx:163
#: src/components/dialogs/nuxs/BookmarksAnnouncement.tsx:171
#: src/components/dialogs/nuxs/DraftsAnnouncement.tsx:138
#: src/components/dialogs/nuxs/InitialVerificationAnnouncement.tsx:175
#: src/components/dialogs/nuxs/InitialVerificationAnnouncement.tsx:184
#: src/components/dialogs/nuxs/LiveNowBetaDialog.tsx:190
@@ -2108,7 +2114,7 @@ msgstr ""
#: src/components/live/EditLiveDialog.tsx:221
#: src/components/NewskieDialog.tsx:167
#: src/components/NewskieDialog.tsx:173
#: src/components/Post/Embed/ExternalEmbed/Gif.tsx:204
#: src/components/Post/Embed/VideoEmbed/GifPresentationControls.tsx:102
#: src/components/ProgressGuide/FollowDialog.tsx:447
#: src/components/StarterPack/Wizard/WizardEditListDialog.tsx:118
#: src/components/StarterPack/Wizard/WizardEditListDialog.tsx:124
@@ -2253,7 +2259,11 @@ msgstr ""
msgid "Compose reply"
msgstr ""
#: src/view/com/composer/Composer.tsx:2295
#: src/view/com/composer/Composer.tsx:2298
msgid "Compressing GIF..."
msgstr ""
#: src/view/com/composer/Composer.tsx:2300
msgid "Compressing video..."
msgstr ""
@@ -2978,7 +2988,7 @@ msgstr ""
msgid "Disable replies entirely"
msgstr ""
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx:387
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx:404
msgid "Disable subtitles"
msgstr ""
@@ -3170,9 +3180,10 @@ msgstr ""
msgid "Doxxing"
msgstr ""
#: src/components/dialogs/nuxs/DraftsAnnouncement.tsx:118
#: src/view/com/composer/drafts/DraftsButton.tsx:65
#: src/view/com/composer/drafts/DraftsButton.tsx:74
#: src/view/com/composer/drafts/DraftsListDialog.tsx:111
#: src/view/com/composer/drafts/DraftsListDialog.tsx:112
msgid "Drafts"
msgstr ""
@@ -3425,7 +3436,7 @@ msgstr ""
msgid "Enable quote posts of this post"
msgstr ""
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx:388
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx:405
msgid "Enable subtitles"
msgstr ""
@@ -3476,7 +3487,7 @@ msgstr ""
msgid "Enter code"
msgstr ""
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx:406
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx:423
msgid "Enter fullscreen"
msgstr ""
@@ -3513,7 +3524,7 @@ msgstr ""
msgid "Enter your username and password"
msgstr ""
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:132
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:146
msgid "Enters full screen"
msgstr ""
@@ -3521,7 +3532,7 @@ msgstr ""
msgid "Entertainment"
msgstr ""
#: src/view/com/composer/Composer.tsx:2304
#: src/view/com/composer/Composer.tsx:2318
#: src/view/com/util/error/ErrorScreen.tsx:42
msgid "Error"
msgstr ""
@@ -3582,7 +3593,7 @@ msgstr ""
msgid "Excludes users you follow"
msgstr ""
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx:405
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx:422
msgid "Exit fullscreen"
msgstr ""
@@ -3610,7 +3621,7 @@ msgstr ""
msgid "Expands or collapses post text"
msgstr ""
#: src/lib/api/index.ts:418
#: src/lib/api/index.ts:420
msgid "Expected uri to resolve to a record"
msgstr ""
@@ -4021,6 +4032,10 @@ msgstr ""
msgid "Finalizing"
msgstr ""
#: src/components/dialogs/nuxs/DraftsAnnouncement.tsx:144
msgid "Finally!"
msgstr ""
#: src/components/dialogs/nuxs/BookmarksAnnouncement.tsx:154
msgid "Finally! Keep track of posts that matter to you. Save them to revisit anytime."
msgstr ""
@@ -4389,10 +4404,15 @@ msgstr ""
msgid "Get started"
msgstr ""
#: src/components/MediaPreview.tsx:114
#: src/components/MediaPreview.tsx:136
#: src/components/Post/Embed/VideoEmbed/GifPresentationControls.tsx:64
msgid "GIF"
msgstr ""
#: src/view/com/composer/Composer.tsx:2323
msgid "GIF uploaded"
msgstr ""
#: src/screens/Onboarding/StepProfile/index.tsx:242
msgid "Give your profile a face"
msgstr ""
@@ -5009,6 +5029,10 @@ msgstr ""
msgid "Introducing activity notifications"
msgstr ""
#: src/components/dialogs/nuxs/DraftsAnnouncement.tsx:48
msgid "Introducing drafts"
msgstr ""
#: src/components/dialogs/nuxs/FindContactsAnnouncement.tsx:55
msgid "Introducing finding friends via contacts"
msgstr ""
@@ -5846,7 +5870,7 @@ msgstr ""
msgid "Music"
msgstr ""
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:153
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:167
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VolumeControl.tsx:97
msgctxt "video"
msgid "Mute"
@@ -6020,6 +6044,7 @@ msgstr ""
#: src/components/dialogs/nuxs/ActivitySubscriptions.tsx:74
#: src/components/dialogs/nuxs/BookmarksAnnouncement.tsx:73
#: src/components/dialogs/nuxs/DraftsAnnouncement.tsx:84
#: src/components/dialogs/nuxs/InitialVerificationAnnouncement.tsx:64
msgid "New Feature"
msgstr ""
@@ -6151,7 +6176,7 @@ msgstr ""
msgid "No DNS Panel"
msgstr ""
#: src/view/com/composer/drafts/DraftsListDialog.tsx:135
#: src/view/com/composer/drafts/DraftsListDialog.tsx:136
msgid "No drafts yet"
msgstr ""
@@ -6339,6 +6364,10 @@ msgstr ""
msgid "Not Found"
msgstr ""
#: src/components/dialogs/nuxs/DraftsAnnouncement.tsx:129
msgid "Not ready to hit post? Keep your best ideas in Drafts until the timing is just right."
msgstr ""
#: src/view/com/profile/ProfileMenu.tsx:558
msgid "Note about sharing"
msgstr ""
@@ -6466,11 +6495,11 @@ msgstr ""
msgid "One or more images is missing alt text."
msgstr ""
#: src/view/com/composer/SelectMediaButton.tsx:393
#: src/view/com/composer/SelectMediaButton.tsx:411
msgid "One or more of your selected files are not supported."
msgstr ""
#: src/view/com/composer/SelectMediaButton.tsx:416
#: src/view/com/composer/SelectMediaButton.tsx:434
msgid "One or more of your selected files are too large. Maximum size is 100 MB."
msgstr ""
@@ -6639,7 +6668,7 @@ msgid "Opens device camera"
msgstr ""
#. Accessibility hint 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:487
#: src/view/com/composer/SelectMediaButton.tsx:505
msgid "Opens device gallery to select up to {MAX_IMAGES, plural, other {# images}}, or a single video or GIF."
msgstr ""
@@ -6812,13 +6841,13 @@ msgstr ""
msgid "Password updated!"
msgstr ""
#: src/components/Post/Embed/ExternalEmbed/Gif.tsx:44
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:137
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx:368
#: src/components/Post/Embed/VideoEmbed/GifPresentationControls.tsx:32
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:151
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx:385
msgid "Pause"
msgstr ""
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx:319
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx:336
msgid "Pause video"
msgstr ""
@@ -6923,9 +6952,9 @@ msgstr ""
msgid "Pinned to your feeds"
msgstr ""
#: src/components/Post/Embed/ExternalEmbed/Gif.tsx:44
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:137
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx:369
#: src/components/Post/Embed/VideoEmbed/GifPresentationControls.tsx:32
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:151
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx:386
msgid "Play"
msgstr ""
@@ -6934,7 +6963,7 @@ msgid "Play {0}"
msgstr ""
#: src/components/Post/Embed/VideoEmbed/index.tsx:115
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx:320
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx:337
msgid "Play video"
msgstr ""
@@ -6942,11 +6971,11 @@ msgstr ""
msgid "Play Video"
msgstr ""
#: src/components/Post/Embed/ExternalEmbed/Gif.tsx:43
#: src/components/Post/Embed/VideoEmbed/GifPresentationControls.tsx:31
msgid "Plays or pauses the GIF"
msgstr ""
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:138
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:152
msgid "Plays or pauses the video"
msgstr ""
@@ -7315,7 +7344,11 @@ msgstr ""
msgid "Privacy violation of a minor"
msgstr ""
#: src/view/com/composer/Composer.tsx:2301
#: src/view/com/composer/Composer.tsx:2312
msgid "Processing GIF..."
msgstr ""
#: src/view/com/composer/Composer.tsx:2314
msgid "Processing video..."
msgstr ""
@@ -8523,7 +8556,7 @@ msgstr ""
msgid "Select your preferred notification channels"
msgstr ""
#: src/view/com/composer/SelectMediaButton.tsx:396
#: src/view/com/composer/SelectMediaButton.tsx:414
msgid "Selecting multiple media types is not supported."
msgstr ""
@@ -8785,7 +8818,7 @@ msgstr ""
msgid "Show"
msgstr ""
#: src/components/Post/Embed/ExternalEmbed/Gif.tsx:183
#: src/components/Post/Embed/VideoEmbed/GifPresentationControls.tsx:81
msgid "Show alt text"
msgstr ""
@@ -9018,6 +9051,10 @@ msgstr ""
msgid "Snoozes the reminder"
msgstr ""
#: src/view/com/composer/drafts/DraftsListDialog.tsx:148
msgid "So many thoughts, you should post one"
msgstr ""
#: src/components/WelcomeModal.tsx:150
msgid "Social media you control."
msgstr ""
@@ -10022,7 +10059,7 @@ msgstr ""
msgid "Toggle to enable or disable adult content"
msgstr ""
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:155
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:169
msgid "Toggles the sound"
msgstr ""
@@ -10263,7 +10300,7 @@ msgstr ""
msgid "Unlike ({0, plural, one {# like} other {# likes}})"
msgstr ""
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:152
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:166
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VolumeControl.tsx:96
msgctxt "video"
msgid "Unmute"
@@ -10300,7 +10337,7 @@ msgstr ""
msgid "Unmute thread"
msgstr ""
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx:317
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx:334
msgid "Unmute video"
msgstr ""
@@ -10438,16 +10475,20 @@ msgstr ""
msgid "Upload from Library"
msgstr ""
#: src/view/com/composer/Composer.tsx:2305
msgid "Uploading GIF..."
msgstr ""
#: src/lib/api/index.ts:302
msgid "Uploading images..."
msgstr ""
#: src/lib/api/index.ts:368
#: src/lib/api/index.ts:392
#: src/lib/api/index.ts:370
#: src/lib/api/index.ts:394
msgid "Uploading link thumbnail..."
msgstr ""
#: src/view/com/composer/Composer.tsx:2298
#: src/view/com/composer/Composer.tsx:2307
msgid "Uploading video..."
msgstr ""
@@ -10676,8 +10717,8 @@ msgstr ""
msgid "Version {0}"
msgstr ""
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:81
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:131
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:84
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:145
msgid "Video"
msgstr ""
@@ -10710,7 +10751,7 @@ msgstr ""
msgid "Video is playing"
msgstr ""
#: src/components/Post/Embed/VideoEmbed/index.web.tsx:219
#: src/components/Post/Embed/VideoEmbed/index.web.tsx:232
msgid "Video not found."
msgstr ""
@@ -10718,11 +10759,11 @@ msgstr ""
msgid "Video settings"
msgstr ""
#: src/view/com/composer/Composer.tsx:2308
#: src/view/com/composer/Composer.tsx:2325
msgid "Video uploaded"
msgstr ""
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:81
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:84
msgid "Video: {0}"
msgstr ""
@@ -10730,7 +10771,7 @@ msgstr ""
msgid "Videos"
msgstr ""
#: src/view/com/composer/SelectMediaButton.tsx:410
#: src/view/com/composer/SelectMediaButton.tsx:428
msgid "Videos must be less than 3 minutes long."
msgstr ""
@@ -11345,11 +11386,11 @@ msgstr ""
msgid "You can now sign in with your new password."
msgstr ""
#: src/view/com/composer/SelectMediaButton.tsx:413
#: src/view/com/composer/SelectMediaButton.tsx:431
msgid "You can only select one GIF at a time."
msgstr ""
#: src/view/com/composer/SelectMediaButton.tsx:407
#: src/view/com/composer/SelectMediaButton.tsx:425
msgid "You can only select one video at a time."
msgstr ""
@@ -11358,7 +11399,7 @@ msgid "You can reactivate your account to continue logging in. Your profile and
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:399
#: src/view/com/composer/SelectMediaButton.tsx:417
msgid "You can select up to {MAX_IMAGES, plural, other {# images}} in total."
msgstr ""
@@ -11547,7 +11588,7 @@ msgstr ""
msgid "You must grant access to your photo library to save a QR code"
msgstr ""
#: src/view/com/composer/SelectMediaButton.tsx:442
#: src/view/com/composer/SelectMediaButton.tsx:460
msgid "You need to allow access to your media library."
msgstr ""
@@ -11723,7 +11764,7 @@ msgstr ""
msgid "Your birth date"
msgstr ""
#: src/components/Post/Embed/VideoEmbed/index.web.tsx:223
#: src/components/Post/Embed/VideoEmbed/index.web.tsx:236
msgid "Your browser does not support the video format. Please try a different browser."
msgstr ""
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+6
View File
@@ -14,6 +14,7 @@ export enum Nux {
FindContactsDismissibleBanner = 'FindContactsDismissibleBanner',
LiveNowBetaDialog = 'LiveNowBetaDialog',
LiveNowBetaNudge = 'LiveNowBetaNudge',
DraftsAnnouncement = 'DraftsAnnouncement',
/*
* Blocking announcements. New IDs are required for each new announcement.
@@ -72,6 +73,10 @@ export type AppNux = BaseNux<
id: Nux.LiveNowBetaNudge
data: undefined
}
| {
id: Nux.DraftsAnnouncement
data: undefined
}
>
export const NuxSchemas: Record<Nux, zod.ZodObject<any> | undefined> = {
@@ -87,4 +92,5 @@ export const NuxSchemas: Record<Nux, zod.ZodObject<any> | undefined> = {
[Nux.FindContactsDismissibleBanner]: undefined,
[Nux.LiveNowBetaDialog]: undefined,
[Nux.LiveNowBetaNudge]: undefined,
[Nux.DraftsAnnouncement]: undefined,
}
+22 -4
View File
@@ -2290,22 +2290,40 @@ function VideoUploadToolbar({state}: {state: VideoState}) {
let text = ''
const isGif = state.video?.mimeType === 'image/gif'
switch (state.status) {
case 'compressing':
text = _(msg`Compressing video...`)
if (isGif) {
text = _(msg`Compressing GIF...`)
} else {
text = _(msg`Compressing video...`)
}
break
case 'uploading':
text = _(msg`Uploading video...`)
if (isGif) {
text = _(msg`Uploading GIF...`)
} else {
text = _(msg`Uploading video...`)
}
break
case 'processing':
text = _(msg`Processing video...`)
if (isGif) {
text = _(msg`Processing GIF...`)
} else {
text = _(msg`Processing video...`)
}
break
case 'error':
text = _(msg`Error`)
wheelProgress = 100
break
case 'done':
text = _(msg`Video uploaded`)
if (isGif) {
text = _(msg`GIF uploaded`)
} else {
text = _(msg`Video uploaded`)
}
break
}
+23 -5
View File
@@ -1,5 +1,6 @@
import {useCallback, useEffect, useRef} from 'react'
import {Keyboard} from 'react-native'
import {File} from 'expo-file-system'
import {type ImagePickerAsset} from 'expo-image-picker'
import {msg, plural} from '@lingui/macro'
import {useLingui} from '@lingui/react'
@@ -18,6 +19,7 @@ import {useSheetWrapper} from '#/components/Dialog/sheet-wrapper'
import {Image_Stroke2_Corner0_Rounded as ImageIcon} from '#/components/icons/Image'
import * as toast from '#/components/Toast'
import {IS_NATIVE, IS_WEB} from '#/env'
import {isAnimatedGif} from './videos/isAnimatedGif'
export type SelectMediaButtonProps = {
disabled?: boolean
@@ -128,7 +130,7 @@ const extensionToMimeType: Record<
* `mimeType`. If `mimeType` is not available, we try to infer it through
* various means.
*/
function classifyImagePickerAsset(asset: ImagePickerAsset):
async function classifyImagePickerAsset(asset: ImagePickerAsset): Promise<
| {
success: true
type: AssetType
@@ -138,7 +140,8 @@ function classifyImagePickerAsset(asset: ImagePickerAsset):
success: false
type: undefined
mimeType: undefined
} {
}
> {
/*
* Try to use the `mimeType` reported by `expo-image-picker` first.
*/
@@ -178,7 +181,22 @@ function classifyImagePickerAsset(asset: ImagePickerAsset):
*/
let type: AssetType | undefined
if (mimeType === 'image/gif') {
type = 'gif'
let bytes: ArrayBuffer | undefined
if (IS_WEB) {
bytes = await asset.file?.arrayBuffer()
} else {
const file = new File(asset.uri)
if (file.exists) {
bytes = await file.arrayBuffer()
}
}
if (bytes) {
const {isAnimated} = isAnimatedGif(bytes)
type = isAnimated ? 'gif' : 'image'
} else {
// If we can't read the file, assume it's animated
type = 'gif'
}
} else if (mimeType?.startsWith('video/')) {
type = 'video'
} else if (mimeType?.startsWith('image/')) {
@@ -236,7 +254,7 @@ async function processImagePickerAssets(
let supportedAssets: ValidatedImagePickerAsset[] = []
for (const asset of assets) {
const {success, type, mimeType} = classifyImagePickerAsset(asset)
const {success, type, mimeType} = await classifyImagePickerAsset(asset)
if (!success) {
errors.add(SelectedAssetError.Unsupported)
@@ -469,7 +487,7 @@ export function SelectMediaButton({
useEffect(() => {
if (autoOpen && !hasAutoOpened.current && !disabled) {
hasAutoOpened.current = true
onPressSelectMedia()
void onPressSelectMedia()
}
}, [autoOpen, disabled, onPressSelectMedia])
@@ -11,6 +11,7 @@ import * as Dialog from '#/components/Dialog'
import {PageX_Stroke2_Corner0_Rounded_Large as PageXIcon} from '#/components/icons/PageX'
import {ListFooter} from '#/components/Lists'
import {Loader} from '#/components/Loader'
import {Text} from '#/components/Typography'
import {useAnalytics} from '#/analytics'
import {IS_NATIVE} from '#/env'
import {DraftItem} from './DraftItem'
@@ -140,13 +141,22 @@ export function DraftsListDialog({
const footerComponent = useMemo(
() => (
<ListFooter
isFetchingNextPage={isFetchingNextPage}
hasNextPage={hasNextPage}
style={[a.border_transparent]}
/>
<>
{drafts.length > 5 && (
<View style={[a.align_center, a.py_2xl]}>
<Text style={[a.text_center, t.atoms.text_contrast_medium]}>
<Trans>So many thoughts, you should post one</Trans>
</Text>
</View>
)}
<ListFooter
isFetchingNextPage={isFetchingNextPage}
hasNextPage={hasNextPage}
style={[a.border_transparent]}
/>
</>
),
[isFetchingNextPage, hasNextPage],
[isFetchingNextPage, hasNextPage, drafts.length, t],
)
return (
@@ -1,4 +1,4 @@
import React from 'react'
import {useRef} from 'react'
import {View} from 'react-native'
import {Image} from 'expo-image'
import {type ImagePickerAsset} from 'expo-image-picker'
@@ -24,7 +24,7 @@ export function VideoPreview({
clear: () => void
}) {
const t = useTheme()
const playerRef = React.useRef<BlueskyVideoView>(null)
const playerRef = useRef<BlueskyVideoView>(null)
const autoplayDisabled = useAutoplayDisabled()
let aspectRatio = asset.width / asset.height
@@ -0,0 +1,59 @@
/**
* Checks if a GIF is animated. Cooked up by Claude, validated with some examples.
* @param bytes - The GIF bytes, as a Uint8Array.
* @returns An object with properties isGif, isAnimated, and frames.
*/
export function isAnimatedGif(buffer: ArrayBuffer): {
isGif: boolean
isAnimated: boolean
frames: number
} {
const bytes = new Uint8Array(buffer)
// Verify GIF signature
const sig = String.fromCharCode(...bytes.slice(0, 6))
if (!sig.startsWith('GIF'))
return {isGif: false, isAnimated: false, frames: 0}
let i = 13 // Skip header + logical screen descriptor
// Skip global color table if present
if (bytes[10] & 0x80) {
const gctSize = 3 * (1 << ((bytes[10] & 0x07) + 1))
i += gctSize
}
let frames = 0
while (i < bytes.length) {
const block = bytes[i++]
if (block === 0x2c) {
// Image descriptor
frames++
// Skip image descriptor fields
i += 8
// Skip local color table if present
if (bytes[i] & 0x80) {
const lctSize = 3 * (1 << ((bytes[i] & 0x07) + 1))
i += lctSize + 1
} else {
i++
}
// Skip image data blocks
i++ // LZW minimum code size
while (bytes[i]) i += bytes[i] + 1
i++
} else if (block === 0x21) {
// Extension
i++ // Extension type
while (bytes[i]) i += bytes[i] + 1
i++
} else if (block === 0x3b) {
// Trailer
break
}
}
return {isGif: true, isAnimated: frames > 1, frames}
}