diff options
Diffstat (limited to 'src/view/com/util/post-embeds')
11 files changed, 77 insertions, 81 deletions
diff --git a/src/view/com/util/post-embeds/ExternalGifEmbed.tsx b/src/view/com/util/post-embeds/ExternalGifEmbed.tsx index 6db4d6fef..39c1d109e 100644 --- a/src/view/com/util/post-embeds/ExternalGifEmbed.tsx +++ b/src/view/com/util/post-embeds/ExternalGifEmbed.tsx @@ -1,16 +1,11 @@ import React from 'react' -import { - ActivityIndicator, - GestureResponderEvent, - LayoutChangeEvent, - Pressable, -} from 'react-native' -import {Image, ImageLoadEventData} from 'expo-image' +import {ActivityIndicator, GestureResponderEvent, Pressable} from 'react-native' +import {Image} from 'expo-image' import {AppBskyEmbedExternal} from '@atproto/api' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' -import {EmbedPlayerParams, getGifDims} from '#/lib/strings/embed-player' +import {EmbedPlayerParams} from '#/lib/strings/embed-player' import {isIOS, isNative, isWeb} from '#/platform/detection' import {useExternalEmbedsPrefs} from '#/state/preferences' import {atoms as a, useTheme} from '#/alf' @@ -28,20 +23,15 @@ export function ExternalGifEmbed({ }) { const t = useTheme() const externalEmbedsPrefs = useExternalEmbedsPrefs() - const {_} = useLingui() const consentDialogControl = useDialogControl() - const thumbHasLoaded = React.useRef(false) - const viewWidth = React.useRef(0) - // Tracking if the placer has been activated const [isPlayerActive, setIsPlayerActive] = React.useState(false) // Tracking whether the gif has been loaded yet const [isPrefetched, setIsPrefetched] = React.useState(false) // Tracking whether the image is animating const [isAnimating, setIsAnimating] = React.useState(true) - const [imageDims, setImageDims] = React.useState({height: 100, width: 1}) // Used for controlling animation const imageRef = React.useRef<Image>(null) @@ -93,16 +83,6 @@ export function ExternalGifEmbed({ ], ) - const onLoad = React.useCallback((e: ImageLoadEventData) => { - if (thumbHasLoaded.current) return - setImageDims(getGifDims(e.source.height, e.source.width, viewWidth.current)) - thumbHasLoaded.current = true - }, []) - - const onLayout = React.useCallback((e: LayoutChangeEvent) => { - viewWidth.current = e.nativeEvent.layout.width - }, []) - return ( <> <EmbedConsentDialog @@ -113,7 +93,7 @@ export function ExternalGifEmbed({ <Pressable style={[ - {height: imageDims.height}, + {height: 300}, a.w_full, a.overflow_hidden, { @@ -122,7 +102,6 @@ export function ExternalGifEmbed({ }, ]} onPress={onPlayPress} - onLayout={onLayout} accessibilityRole="button" accessibilityHint={_(msg`Plays the GIF`)} accessibilityLabel={_(msg`Play ${link.title}`)}> @@ -135,7 +114,6 @@ export function ExternalGifEmbed({ }} // Web uses the thumb to control playback style={{flex: 1}} ref={imageRef} - onLoad={onLoad} autoplay={isAnimating} contentFit="contain" accessibilityIgnoresInvertColors diff --git a/src/view/com/util/post-embeds/VideoEmbed.tsx b/src/view/com/util/post-embeds/VideoEmbed.tsx index 24802d188..f268bf8db 100644 --- a/src/view/com/util/post-embeds/VideoEmbed.tsx +++ b/src/view/com/util/post-embeds/VideoEmbed.tsx @@ -1,5 +1,5 @@ import React, {useCallback, useState} from 'react' -import {View} from 'react-native' +import {ActivityIndicator, View} from 'react-native' import {ImageBackground} from 'expo-image' import {AppBskyEmbedVideo} from '@atproto/api' import {msg, Trans} from '@lingui/macro' @@ -10,7 +10,6 @@ import {VideoEmbedInnerNative} from '#/view/com/util/post-embeds/VideoEmbedInner import {atoms as a} from '#/alf' import {Button} from '#/components/Button' import {useThrottledValue} from '#/components/hooks/useThrottledValue' -import {Loader} from '#/components/Loader' import {PlayButtonIcon} from '#/components/video/PlayButtonIcon' import {ErrorBoundary} from '../ErrorBoundary' import * as VideoFallback from './VideoEmbedInner/VideoFallback' @@ -89,12 +88,9 @@ function InnerWrapper({embed}: Props) { source={{uri: embed.thumbnail}} accessibilityIgnoresInvertColors style={[ + a.absolute, + a.inset_0, { - position: 'absolute', - top: 0, - left: 0, - right: 0, - bottom: 0, backgroundColor: 'transparent', // If you don't add `backgroundColor` to the styles here, // the play button won't show up on the first render on android 🥴😮💨 display: showOverlay ? 'flex' : 'none', @@ -102,27 +98,29 @@ function InnerWrapper({embed}: Props) { ]} cachePolicy="memory-disk" // Preferring memory cache helps to avoid flicker when re-displaying on android > - <Button - style={[a.flex_1, a.align_center, a.justify_center]} - onPress={() => { - ref.current?.togglePlayback() - }} - label={_(msg`Play video`)} - color="secondary"> - {showSpinner ? ( - <View - style={[ - a.rounded_full, - a.p_xs, - a.align_center, - a.justify_center, - ]}> - <Loader size="2xl" style={{color: 'white'}} /> - </View> - ) : ( - <PlayButtonIcon /> - )} - </Button> + {showOverlay && ( + <Button + style={[a.flex_1, a.align_center, a.justify_center]} + onPress={() => { + ref.current?.togglePlayback() + }} + label={_(msg`Play video`)} + color="secondary"> + {showSpinner ? ( + <View + style={[ + a.rounded_full, + a.p_xs, + a.align_center, + a.justify_center, + ]}> + <ActivityIndicator size="large" color="white" /> + </View> + ) : ( + <PlayButtonIcon /> + )} + </Button> + )} </ImageBackground> </> ) diff --git a/src/view/com/util/post-embeds/VideoEmbed.web.tsx b/src/view/com/util/post-embeds/VideoEmbed.web.tsx index 3180dd99e..a1f4652ac 100644 --- a/src/view/com/util/post-embeds/VideoEmbed.web.tsx +++ b/src/view/com/util/post-embeds/VideoEmbed.web.tsx @@ -24,6 +24,7 @@ export function VideoEmbed({embed}: {embed: AppBskyEmbedVideo.View}) { useActiveVideoWeb() const [onScreen, setOnScreen] = useState(false) const [isFullscreen] = useFullscreen() + const lastKnownTime = useRef<number | undefined>() useEffect(() => { if (!ref.current) return @@ -82,6 +83,7 @@ export function VideoEmbed({embed}: {embed: AppBskyEmbedVideo.View}) { active={active} setActive={setActive} onScreen={onScreen} + lastKnownTime={lastKnownTime} /> </ViewportObserver> </ErrorBoundary> diff --git a/src/view/com/util/post-embeds/VideoEmbedInner/TimeIndicator.tsx b/src/view/com/util/post-embeds/VideoEmbedInner/TimeIndicator.tsx index 66e1df50d..75e544aca 100644 --- a/src/view/com/util/post-embeds/VideoEmbedInner/TimeIndicator.tsx +++ b/src/view/com/util/post-embeds/VideoEmbedInner/TimeIndicator.tsx @@ -1,8 +1,9 @@ -import React from 'react' import {StyleProp, ViewStyle} from 'react-native' -import Animated, {FadeInDown, FadeOutDown} from 'react-native-reanimated' +import {View} from 'react-native' +import {msg} from '@lingui/macro' +import {useLingui} from '@lingui/react' -import {atoms as a, native, useTheme} from '#/alf' +import {atoms as a, useTheme} from '#/alf' import {Text} from '#/components/Typography' /** @@ -17,6 +18,7 @@ export function TimeIndicator({ style?: StyleProp<ViewStyle> }) { const t = useTheme() + const {_} = useLingui() if (isNaN(time)) { return null @@ -26,10 +28,10 @@ export function TimeIndicator({ const seconds = String(time % 60).padStart(2, '0') return ( - <Animated.View - entering={native(FadeInDown.duration(300))} - exiting={native(FadeOutDown.duration(500))} + <View pointerEvents="none" + accessibilityLabel={_(msg`Time remaining: ${time} seconds`)} + accessibilityHint="" style={[ { backgroundColor: 'rgba(0, 0, 0, 0.5)', @@ -52,6 +54,6 @@ export function TimeIndicator({ ]}> {`${minutes}:${seconds}`} </Text> - </Animated.View> + </View> ) } diff --git a/src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx b/src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx index 21db54322..215e4c406 100644 --- a/src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx +++ b/src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx @@ -1,6 +1,5 @@ import React, {useRef} from 'react' import {Pressable, StyleProp, View, ViewStyle} from 'react-native' -import Animated, {FadeInDown} from 'react-native-reanimated' import {AppBskyEmbedVideo} from '@atproto/api' import {BlueskyVideoView} from '@haileyok/bluesky-video' import {msg} from '@lingui/macro' @@ -182,8 +181,7 @@ function ControlButton({ style?: StyleProp<ViewStyle> }) { return ( - <Animated.View - entering={FadeInDown.duration(300)} + <View style={[ a.absolute, a.rounded_full, @@ -207,6 +205,6 @@ function ControlButton({ hitSlop={HITSLOP_30}> {children} </Pressable> - </Animated.View> + </View> ) } diff --git a/src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerWeb.tsx b/src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerWeb.tsx index ef989c4a4..e6882a2f6 100644 --- a/src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerWeb.tsx +++ b/src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerWeb.tsx @@ -1,6 +1,8 @@ import React, {useEffect, useId, useRef, useState} from 'react' import {View} from 'react-native' import {AppBskyEmbedVideo} from '@atproto/api' +import {msg} from '@lingui/macro' +import {useLingui} from '@lingui/react' import type * as HlsTypes from 'hls.js' import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' @@ -13,11 +15,13 @@ export function VideoEmbedInnerWeb({ active, setActive, onScreen, + lastKnownTime, }: { embed: AppBskyEmbedVideo.View active: boolean setActive: () => void onScreen: boolean + lastKnownTime: React.MutableRefObject<number | undefined> }) { const containerRef = useRef<HTMLDivElement>(null) const videoRef = useRef<HTMLVideoElement>(null) @@ -25,6 +29,7 @@ export function VideoEmbedInnerWeb({ const [hasSubtitleTrack, setHasSubtitleTrack] = useState(false) const [hlsLoading, setHlsLoading] = React.useState(false) const figId = useId() + const {_} = useLingui() // send error up to error boundary const [error, setError] = useState<Error | null>(null) @@ -40,8 +45,17 @@ export function VideoEmbedInnerWeb({ setHlsLoading, }) + useEffect(() => { + if (lastKnownTime.current && videoRef.current) { + videoRef.current.currentTime = lastKnownTime.current + } + }, [lastKnownTime]) + return ( - <View style={[a.flex_1, a.rounded_md, a.overflow_hidden]}> + <View + style={[a.flex_1, a.rounded_md, a.overflow_hidden]} + accessibilityLabel={_(msg`Embedded video player`)} + accessibilityHint=""> <div ref={containerRef} style={{height: '100%', width: '100%'}}> <figure style={{margin: 0, position: 'absolute', inset: 0}}> <video @@ -52,6 +66,9 @@ export function VideoEmbedInnerWeb({ preload="none" muted={!focused} aria-labelledby={embed.alt ? figId : undefined} + onTimeUpdate={e => { + lastKnownTime.current = e.currentTarget.currentTime + }} /> {embed.alt && ( <figcaption diff --git a/src/view/com/util/post-embeds/VideoEmbedInner/web-controls/ControlButton.tsx b/src/view/com/util/post-embeds/VideoEmbedInner/web-controls/ControlButton.tsx index 8ffe482a8..651046445 100644 --- a/src/view/com/util/post-embeds/VideoEmbedInner/web-controls/ControlButton.tsx +++ b/src/view/com/util/post-embeds/VideoEmbedInner/web-controls/ControlButton.tsx @@ -23,7 +23,8 @@ export function ControlButton({ return ( <PressableWithHover accessibilityRole="button" - accessibilityHint={active ? activeLabel : inactiveLabel} + accessibilityLabel={active ? activeLabel : inactiveLabel} + accessibilityHint="" onPress={onPress} style={[ a.p_xs, @@ -32,9 +33,9 @@ export function ControlButton({ ]} hoverStyle={{backgroundColor: 'rgba(255, 255, 255, 0.2)'}}> {active ? ( - <ActiveIcon fill={t.palette.white} width={20} /> + <ActiveIcon fill={t.palette.white} width={20} aria-hidden /> ) : ( - <InactiveIcon fill={t.palette.white} width={20} /> + <InactiveIcon fill={t.palette.white} width={20} aria-hidden /> )} </PressableWithHover> ) diff --git a/src/view/com/util/post-embeds/VideoEmbedInner/web-controls/Scrubber.tsx b/src/view/com/util/post-embeds/VideoEmbedInner/web-controls/Scrubber.tsx index 44978ad51..74aad64e1 100644 --- a/src/view/com/util/post-embeds/VideoEmbedInner/web-controls/Scrubber.tsx +++ b/src/view/com/util/post-embeds/VideoEmbedInner/web-controls/Scrubber.tsx @@ -186,7 +186,9 @@ export function Scrubber({ </View> <div ref={circleRef} - aria-label={_(msg`Seek slider`)} + aria-label={_( + msg`Seek slider. Use the arrow keys to seek forwards and backwards, and space to play/pause`, + )} role="slider" aria-valuemax={duration} aria-valuemin={0} diff --git a/src/view/com/util/post-embeds/VideoEmbedInner/web-controls/VideoControls.tsx b/src/view/com/util/post-embeds/VideoEmbedInner/web-controls/VideoControls.tsx index acd4d1aae..8e134d221 100644 --- a/src/view/com/util/post-embeds/VideoEmbedInner/web-controls/VideoControls.tsx +++ b/src/view/com/util/post-embeds/VideoEmbedInner/web-controls/VideoControls.tsx @@ -313,13 +313,14 @@ export function Controls({ onPointerEnter={onPointerMoveEmptySpace} onPointerMove={onPointerMoveEmptySpace} onPointerLeave={onPointerLeaveEmptySpace} - accessibilityHint={_( + accessibilityLabel={_( !focused ? msg`Unmute video` : playing ? msg`Pause video` : msg`Play video`, )} + accessibilityHint="" style={[ a.flex_1, web({cursor: showCursor || !playing ? 'pointer' : 'none'}), @@ -401,7 +402,7 @@ export function Controls({ <ControlButton active={isFullscreen} activeLabel={_(msg`Exit fullscreen`)} - inactiveLabel={_(msg`Fullscreen`)} + inactiveLabel={_(msg`Enter fullscreen`)} activeIcon={ArrowsInIcon} inactiveIcon={ArrowsOutIcon} onPress={onPressFullscreen} diff --git a/src/view/com/util/post-embeds/VideoEmbedInner/web-controls/VolumeControl.tsx b/src/view/com/util/post-embeds/VideoEmbedInner/web-controls/VolumeControl.tsx index 63ac32b10..90ffb9e6b 100644 --- a/src/view/com/util/post-embeds/VideoEmbedInner/web-controls/VolumeControl.tsx +++ b/src/view/com/util/post-embeds/VideoEmbedInner/web-controls/VolumeControl.tsx @@ -77,6 +77,7 @@ export function VolumeControl({ min={0} max={100} value={sliderVolume} + aria-label={_(msg`Volume`)} style={ // Ridiculous safari hack for old version of safari. Fixed in sonoma beta -h isSafari ? {height: 92, minHeight: '100%'} : {height: '100%'} diff --git a/src/view/com/util/post-embeds/index.tsx b/src/view/com/util/post-embeds/index.tsx index 1351a2cbc..9dc43da8e 100644 --- a/src/view/com/util/post-embeds/index.tsx +++ b/src/view/com/util/post-embeds/index.tsx @@ -6,13 +6,7 @@ import { View, ViewStyle, } from 'react-native' -import { - AnimatedRef, - measure, - MeasuredDimensions, - runOnJS, - runOnUI, -} from 'react-native-reanimated' +import {MeasuredDimensions, runOnJS, runOnUI} from 'react-native-reanimated' import {Image} from 'expo-image' import { AppBskyEmbedExternal, @@ -27,6 +21,7 @@ import { ModerationDecision, } from '@atproto/api' +import {HandleRef, measureHandle} from '#/lib/hooks/useHandleRef' import {usePalette} from '#/lib/hooks/usePalette' import {useLightboxControls} from '#/state/lightbox' import {useModerationOpts} from '#/state/preferences/moderation-opts' @@ -163,12 +158,13 @@ export function PostEmbeds({ } const onPress = ( index: number, - refs: AnimatedRef<React.Component<{}, {}, any>>[], + refs: HandleRef[], fetchedDims: (Dimensions | null)[], ) => { + const handles = refs.map(r => r.current) runOnUI(() => { 'worklet' - const rects = refs.map(ref => (ref ? measure(ref) : null)) + const rects = handles.map(measureHandle) runOnJS(_openLightbox)(index, rects, fetchedDims) })() } |