From b1b0ad09ccfb27eb168e64858409f9c9dff27933 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Thu, 25 Jun 2026 16:52:09 -0500 Subject: [PATCH] Video alt text (#10990) Co-authored-by: Zaven477 <77801431+Zaven477@users.noreply.github.com> --- src/components/AltBadgeWithDialog.tsx | 93 ++++++++++++++ .../Post/Embed/ExternalEmbed/Gif.tsx | 14 +-- .../VideoEmbed/GifPresentationControls.tsx | 113 ++++++------------ .../VideoEmbedInner/VideoEmbedInnerNative.tsx | 32 +++-- .../VideoEmbedInner/VideoEmbedInnerWeb.tsx | 9 +- 5 files changed, 159 insertions(+), 102 deletions(-) create mode 100644 src/components/AltBadgeWithDialog.tsx diff --git a/src/components/AltBadgeWithDialog.tsx b/src/components/AltBadgeWithDialog.tsx new file mode 100644 index 0000000000..07cf9544f0 --- /dev/null +++ b/src/components/AltBadgeWithDialog.tsx @@ -0,0 +1,93 @@ +import {Pressable} from 'react-native' +import {Trans, useLingui} from '@lingui/react/macro' + +import {HITSLOP_20} from '#/lib/constants' +import {useLargeAltBadgeEnabled} from '#/state/preferences/large-alt-badge' +import {atoms as a, useTheme} from '#/alf' +import * as Prompt from '#/components/Prompt' +import {Text} from '#/components/Typography' + +const positionStyles = { + 'top-left': { + top: a.p_xs.padding, + left: a.p_xs.padding, + }, + 'top-right': { + top: a.p_xs.padding, + right: a.p_xs.padding, + }, + 'bottom-left': { + bottom: a.p_xs.padding, + left: a.p_xs.padding, + }, + 'bottom-right': { + bottom: a.p_xs.padding, + right: a.p_xs.padding, + }, +} + +export function AltBadgeWithDialog({ + text, + position, +}: { + text: string + position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' +}) { + const t = useTheme() + const {t: l} = useLingui() + const large = useLargeAltBadgeEnabled() + const control = Prompt.usePromptControl() + + const pos = position ? [a.absolute, positionStyles[position]] : {} + + return ( + <> + [ + a.justify_center, + a.rounded_sm, + a.p_xs, + a.z_10, + t.atoms.bg_contrast_25, + large && { + padding: 6, + }, + { + opacity: 0.8, + }, + pos, + s.hovered || s.pressed + ? [ + { + opacity: 1, + }, + ] + : [], + ]}> + + ALT + + + + + + + Alt Text + + {text} + + + + + + + ) +} diff --git a/src/components/Post/Embed/ExternalEmbed/Gif.tsx b/src/components/Post/Embed/ExternalEmbed/Gif.tsx index 4cf95e0fab..24ea286ada 100644 --- a/src/components/Post/Embed/ExternalEmbed/Gif.tsx +++ b/src/components/Post/Embed/ExternalEmbed/Gif.tsx @@ -66,19 +66,7 @@ export function GifEmbed({ a.overflow_hidden, {backgroundColor: t.palette.black}, ]}> - + @@ -64,74 +60,41 @@ export function GifPresentationControls({ ]} /> )} - - - GIF - + + + + GIF + + + {altText && } - {altText && } ) } - -function AltBadge({text}: {text: string}) { - const control = Prompt.usePromptControl() - const {_} = useLingui() - - return ( - <> - - - ALT - - - - - - Alt Text - - {text} - - - control.close()} - cta={_(msg`Close`)} - color="secondary" - /> - - - - ) -} - -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, - }, -}) diff --git a/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx index 05e8b84581..f38677ea1d 100644 --- a/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx +++ b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx @@ -8,6 +8,7 @@ import {useLingui} from '@lingui/react' import {HITSLOP_30} from '#/lib/constants' import {useAutoplayDisabled} from '#/state/preferences' import {atoms as a, useTheme} from '#/alf' +import {AltBadgeWithDialog} from '#/components/AltBadgeWithDialog' import {useIsWithinMessage} from '#/components/dms/MessageContext' import {Mute_Stroke2_Corner0_Rounded as MuteIcon} from '#/components/icons/Mute' import {Pause_Filled_Corner0_Rounded as PauseIcon} from '#/components/icons/Pause' @@ -98,19 +99,24 @@ export function VideoEmbedInnerNative({ altText={embed.alt} /> ) : ( - { - videoRef.current?.enterFullscreen(true) - }} - toggleMuted={() => { - videoRef.current?.toggleMuted() - }} - togglePlayback={() => { - videoRef.current?.togglePlayback() - }} - isPlaying={isPlaying} - timeRemaining={timeRemaining} - /> + <> + { + videoRef.current?.enterFullscreen(true) + }} + toggleMuted={() => { + videoRef.current?.toggleMuted() + }} + togglePlayback={() => { + videoRef.current?.togglePlayback() + }} + isPlaying={isPlaying} + timeRemaining={timeRemaining} + /> + {embed.alt && ( + + )} + )} diff --git a/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerWeb.tsx b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerWeb.tsx index d56c0b234d..b97317dfbf 100644 --- a/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerWeb.tsx +++ b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerWeb.tsx @@ -7,6 +7,8 @@ import type * as HlsTypes from 'hls.js' import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' import {atoms as a} from '#/alf' +import {AltBadgeWithDialog} from '#/components/AltBadgeWithDialog' +import {useFullscreen} from '#/components/hooks/useFullscreen' import * as BandwidthEstimate from './bandwidth-estimate' import {Controls} from './web-controls/VideoControls' @@ -30,6 +32,8 @@ export function VideoEmbedInnerWeb({ const [hlsLoading, setHlsLoading] = useState(false) const figId = useId() const {_} = useLingui() + const [isFullscreen] = useFullscreen(containerRef) + const isGif = embed.presentation === 'gif' // send error up to error boundary const [error, setError] = useState(null) @@ -77,6 +81,9 @@ export function VideoEmbedInnerWeb({ )} + {!isFullscreen && !isGif && embed.alt && ( + + )}