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
+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>
)
}