Merge branch 'main' into app-1788
This commit is contained in:
+4
-4
@@ -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 |
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
+19
-2
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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') {
|
||||
|
||||
+515
-375
File diff suppressed because it is too large
Load Diff
+1526
-1423
File diff suppressed because it is too large
Load Diff
+515
-375
File diff suppressed because it is too large
Load Diff
+1526
-1423
File diff suppressed because it is too large
Load Diff
+1526
-1423
File diff suppressed because it is too large
Load Diff
+514
-374
File diff suppressed because it is too large
Load Diff
+533
-393
File diff suppressed because it is too large
Load Diff
+518
-378
File diff suppressed because it is too large
Load Diff
+533
-393
File diff suppressed because it is too large
Load Diff
+515
-375
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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 ""
|
||||
|
||||
|
||||
+533
-393
File diff suppressed because it is too large
Load Diff
+515
-375
File diff suppressed because it is too large
Load Diff
+514
-374
File diff suppressed because it is too large
Load Diff
+515
-375
File diff suppressed because it is too large
Load Diff
+515
-375
File diff suppressed because it is too large
Load Diff
+515
-375
File diff suppressed because it is too large
Load Diff
+515
-375
File diff suppressed because it is too large
Load Diff
+515
-375
File diff suppressed because it is too large
Load Diff
+515
-375
File diff suppressed because it is too large
Load Diff
+515
-375
File diff suppressed because it is too large
Load Diff
+518
-378
File diff suppressed because it is too large
Load Diff
+515
-375
File diff suppressed because it is too large
Load Diff
+515
-375
File diff suppressed because it is too large
Load Diff
+515
-375
File diff suppressed because it is too large
Load Diff
+515
-375
File diff suppressed because it is too large
Load Diff
+1526
-1423
File diff suppressed because it is too large
Load Diff
+515
-375
File diff suppressed because it is too large
Load Diff
+517
-377
File diff suppressed because it is too large
Load Diff
+1526
-1423
File diff suppressed because it is too large
Load Diff
+515
-375
File diff suppressed because it is too large
Load Diff
+515
-375
File diff suppressed because it is too large
Load Diff
+515
-375
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
+514
-374
File diff suppressed because it is too large
Load Diff
+515
-375
File diff suppressed because it is too large
Load Diff
+516
-376
File diff suppressed because it is too large
Load Diff
+515
-375
File diff suppressed because it is too large
Load Diff
+524
-384
File diff suppressed because it is too large
Load Diff
+515
-375
File diff suppressed because it is too large
Load Diff
+515
-375
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
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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}
|
||||
}
|
||||
Reference in New Issue
Block a user