about summary refs log tree commit diff
path: root/src/view/com/util/post-embeds
diff options
context:
space:
mode:
Diffstat (limited to 'src/view/com/util/post-embeds')
-rw-r--r--src/view/com/util/post-embeds/ExternalGifEmbed.tsx30
-rw-r--r--src/view/com/util/post-embeds/VideoEmbed.tsx54
-rw-r--r--src/view/com/util/post-embeds/VideoEmbed.web.tsx2
-rw-r--r--src/view/com/util/post-embeds/VideoEmbedInner/TimeIndicator.tsx16
-rw-r--r--src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx6
-rw-r--r--src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerWeb.tsx19
-rw-r--r--src/view/com/util/post-embeds/VideoEmbedInner/web-controls/ControlButton.tsx7
-rw-r--r--src/view/com/util/post-embeds/VideoEmbedInner/web-controls/Scrubber.tsx4
-rw-r--r--src/view/com/util/post-embeds/VideoEmbedInner/web-controls/VideoControls.tsx5
-rw-r--r--src/view/com/util/post-embeds/VideoEmbedInner/web-controls/VolumeControl.tsx1
-rw-r--r--src/view/com/util/post-embeds/index.tsx14
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)
         })()
       }