align alt text behaviour between gif types (#9822)

This commit is contained in:
Samuel Newman
2026-02-04 22:34:14 +02:00
committed by GitHub
parent 965b17ff86
commit e6c4a539ca
5 changed files with 63 additions and 75 deletions
@@ -1,25 +1,14 @@
import {useRef, useState} from 'react' import {useRef, useState} from 'react'
import { import {type StyleProp, View, type ViewStyle} from 'react-native'
type StyleProp, import {msg} from '@lingui/macro'
StyleSheet,
TouchableOpacity,
View,
type ViewStyle,
} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {HITSLOP_20} from '#/lib/constants'
import {clamp} from '#/lib/numbers' import {clamp} from '#/lib/numbers'
import {type EmbedPlayerParams} from '#/lib/strings/embed-player' import {type EmbedPlayerParams} from '#/lib/strings/embed-player'
import {useAutoplayDisabled} from '#/state/preferences' import {useAutoplayDisabled} from '#/state/preferences'
import {useLargeAltBadgeEnabled} from '#/state/preferences/large-alt-badge'
import {atoms as a, useTheme} from '#/alf' import {atoms as a, useTheme} from '#/alf'
import {Fill} from '#/components/Fill' import {Fill} from '#/components/Fill'
import {MediaInsetBorder} from '#/components/MediaInsetBorder' import {MediaInsetBorder} from '#/components/MediaInsetBorder'
import * as Prompt from '#/components/Prompt'
import {Text} from '#/components/Typography'
import {IS_WEB} from '#/env'
import {GifView} from '../../../../../modules/expo-bluesky-gif-view' import {GifView} from '../../../../../modules/expo-bluesky-gif-view'
import {type GifViewStateChangeEvent} from '../../../../../modules/expo-bluesky-gif-view/src/GifView.types' import {type GifViewStateChangeEvent} from '../../../../../modules/expo-bluesky-gif-view/src/GifView.types'
import {GifPresentationControls} from '../VideoEmbed/GifPresentationControls' import {GifPresentationControls} from '../VideoEmbed/GifPresentationControls'
@@ -94,6 +83,7 @@ export function GifEmbed({
onPress={onPress} onPress={onPress}
isPlaying={playerState.isPlaying} isPlaying={playerState.isPlaying}
isLoading={!playerState.isLoaded} isLoading={!playerState.isLoaded}
altText={!hideAlt && isPreferredAltText ? altText : undefined}
/> />
<GifView <GifView
source={params.playerUri} source={params.playerUri}
@@ -115,68 +105,7 @@ export function GifEmbed({
]} ]}
/> />
)} )}
{!hideAlt && isPreferredAltText && <AltText text={altText} />}
</View> </View>
</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',
},
})
@@ -1,10 +1,12 @@
import {Pressable, StyleSheet, View} from 'react-native' import {Pressable, StyleSheet, TouchableOpacity, View} from 'react-native'
import {msg, Trans} from '@lingui/macro' import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {HITSLOP_20} from '#/lib/constants'
import {atoms as a, useTheme} from '#/alf' import {atoms as a, useTheme} from '#/alf'
import {Fill} from '#/components/Fill' import {Fill} from '#/components/Fill'
import {Loader} from '#/components/Loader' import {Loader} from '#/components/Loader'
import * as Prompt from '#/components/Prompt'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
import {PlayButtonIcon} from '#/components/video/PlayButtonIcon' import {PlayButtonIcon} from '#/components/video/PlayButtonIcon'
@@ -12,10 +14,12 @@ export function GifPresentationControls({
onPress, onPress,
isPlaying, isPlaying,
isLoading, isLoading,
altText,
}: { }: {
onPress: () => void onPress: () => void
isPlaying: boolean isPlaying: boolean
isLoading?: boolean isLoading?: boolean
altText?: string
}) { }) {
const {_} = useLingui() const {_} = useLingui()
const t = useTheme() const t = useTheme()
@@ -60,6 +64,46 @@ export function GifPresentationControls({
<Trans>GIF</Trans> <Trans>GIF</Trans>
</Text> </Text>
</View> </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>
</> </>
) )
} }
@@ -75,4 +119,14 @@ const styles = StyleSheet.create({
bottom: 6, bottom: 6,
zIndex: 2, zIndex: 2,
}, },
altBadgeContainer: {
backgroundColor: 'rgba(0, 0, 0, 0.75)',
borderRadius: 6,
paddingHorizontal: 4,
paddingVertical: 3,
position: 'absolute',
right: 6,
bottom: 6,
zIndex: 2,
},
}) })
@@ -92,6 +92,7 @@ export function VideoEmbedInnerNative({
}} }}
isPlaying={isPlaying} isPlaying={isPlaying}
isLoading={false} isLoading={false}
altText={embed.alt}
/> />
) : ( ) : (
<VideoPresentationControls <VideoPresentationControls
@@ -101,6 +101,7 @@ export function VideoEmbedInnerWeb({
fullscreenRef={containerRef} fullscreenRef={containerRef}
hasSubtitleTrack={hasSubtitleTrack} hasSubtitleTrack={hasSubtitleTrack}
isGif={embed.presentation === 'gif'} isGif={embed.presentation === 'gif'}
altText={embed.alt}
/> />
</div> </div>
</View> </View>
@@ -46,6 +46,7 @@ export function Controls({
hlsLoading, hlsLoading,
hasSubtitleTrack, hasSubtitleTrack,
isGif, isGif,
altText,
}: { }: {
videoRef: React.RefObject<HTMLVideoElement | null> videoRef: React.RefObject<HTMLVideoElement | null>
hlsRef: React.RefObject<Hls | undefined | null> hlsRef: React.RefObject<Hls | undefined | null>
@@ -58,6 +59,7 @@ export function Controls({
hlsLoading: boolean hlsLoading: boolean
hasSubtitleTrack: boolean hasSubtitleTrack: boolean
isGif: boolean isGif: boolean
altText?: string
}) { }) {
const { const {
play, play,
@@ -296,6 +298,7 @@ export function Controls({
isPlaying={playing} isPlaying={playing}
isLoading={showSpinner} isLoading={showSpinner}
onPress={onPressPlayPause} onPress={onPressPlayPause}
altText={altText}
/> />
) )
} }