Add timestamp field to video report dialog (#11339)

This commit is contained in:
DS Boyce
2026-08-06 12:11:05 -07:00
committed by GitHub
parent 724013df1b
commit 1c349ad7da
27 changed files with 402 additions and 119 deletions
@@ -2,8 +2,7 @@ import {useImperativeHandle, useRef, useState} from 'react'
import {Pressable, type StyleProp, View, type ViewStyle} from 'react-native'
import {type AppBskyEmbedVideo} from '@atproto/api'
import {BlueskyVideoView} from '@bsky.app/video'
import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react'
import {useLingui} from '@lingui/react/macro'
import {HITSLOP_30} from '#/lib/constants'
import {useAutoplayDisabled} from '#/state/preferences'
@@ -16,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 {KeepAwake} from '#/components/KeepAwake'
import {MediaInsetBorder} from '#/components/MediaInsetBorder'
import {useReportDialogMetadataContext} from '#/components/moderation/ReportDialog/ReportDialogMetadataContext'
import {useVideoMuteState} from '#/components/Post/Embed/VideoEmbed/VideoVolumeContext'
import {GifPresentationControls} from '../GifPresentationControls'
import {TimeIndicator} from './TimeIndicator'
@@ -39,11 +39,13 @@ export function VideoEmbedInnerNative({
*/
onError?: (error: string) => void
}) {
const {_} = useLingui()
const {t: l} = useLingui()
const videoRef = useRef<BlueskyVideoView>(null)
const autoplayDisabled = useAutoplayDisabled()
const isWithinMessage = useIsWithinMessage()
const [muted, setMuted] = useVideoMuteState()
const reportDialogMetadata = useReportDialogMetadataContext()
const maxTimeRemainingSeconds = useRef(0)
const [isPlaying, setIsPlaying] = useState(false)
const [timeRemaining, setTimeRemaining] = useState(0)
@@ -84,16 +86,30 @@ export function VideoEmbedInnerNative({
setIsPlaying(e.nativeEvent.status === 'playing')
}}
onTimeRemainingChange={e => {
setTimeRemaining(e.nativeEvent.timeRemaining)
const {timeRemaining} = e.nativeEvent
setTimeRemaining(timeRemaining)
if (
!isGif &&
reportDialogMetadata &&
Number.isFinite(timeRemaining) &&
timeRemaining >= 0
) {
maxTimeRemainingSeconds.current = Math.max(
maxTimeRemainingSeconds.current,
timeRemaining,
)
reportDialogMetadata.current.videoTimestampSeconds = Math.max(
0,
maxTimeRemainingSeconds.current - timeRemaining,
)
}
}}
onError={e => {
onError?.(e.nativeEvent.error)
setError(e.nativeEvent.error)
}}
ref={videoRef}
accessibilityLabel={
embed.alt ? _(msg`Video: ${embed.alt}`) : _(msg`Video`)
}
accessibilityLabel={embed.alt ? l`Video: ${embed.alt}` : l`Video`}
accessibilityHint=""
/>
{isGif ? (
@@ -144,7 +160,7 @@ function VideoPresentationControls({
timeRemaining: number
isPlaying: boolean
}) {
const {_} = useLingui()
const {t: l} = useLingui()
const t = useTheme()
const [muted] = useVideoMuteState()
@@ -159,14 +175,14 @@ function VideoPresentationControls({
<Pressable
onPress={enterFullscreen}
style={a.flex_1}
accessibilityLabel={_(msg`Video`)}
accessibilityHint={_(msg`Enters full screen`)}
accessibilityLabel={l`Video`}
accessibilityHint={l`Enters full screen`}
accessibilityRole="button"
/>
<ControlButton
onPress={togglePlayback}
label={isPlaying ? _(msg`Pause`) : _(msg`Play`)}
accessibilityHint={_(msg`Plays or pauses the video`)}
label={isPlaying ? l`Pause` : l`Play`}
accessibilityHint={l`Plays or pauses the video`}
style={{left: 6}}>
{isPlaying ? (
<PauseIcon width={13} fill={t.palette.white} />
@@ -175,15 +191,14 @@ function VideoPresentationControls({
)}
</ControlButton>
{showTime && <TimeIndicator time={timeRemaining} style={{left: 33}} />}
<ControlButton
onPress={toggleMuted}
label={
muted
? _(msg({message: `Unmute`, context: 'video'}))
: _(msg({message: `Mute`, context: 'video'}))
? l({message: `Unmute`, context: 'video'})
: l({message: `Mute`, context: 'video'})
}
accessibilityHint={_(msg`Toggles the sound`)}
accessibilityHint={l`Toggles the sound`}
style={{right: 6}}>
{muted ? (
<MuteIcon width={13} fill={t.palette.white} />
@@ -1,13 +1,13 @@
import {useCallback, useEffect, useId, useRef, useState} from 'react'
import {View} from 'react-native'
import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react'
import {useLingui} from '@lingui/react/macro'
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 {useReportDialogMetadataContext} from '#/components/moderation/ReportDialog/ReportDialogMetadataContext'
import * as BandwidthEstimate from './bandwidth-estimate'
import {
HLSFatalError,
@@ -36,9 +36,10 @@ export function VideoEmbedInnerWeb({
const [hasSubtitleTrack, setHasSubtitleTrack] = useState(false)
const [hlsLoading, setHlsLoading] = useState(false)
const figId = useId()
const {_} = useLingui()
const {t: l} = useLingui()
const [isFullscreen] = useFullscreen(containerRef)
const isGif = embed.presentation === 'gif'
const reportDialogMetadata = useReportDialogMetadataContext()
// send error up to error boundary
const [error, setError] = useState<Error | null>(null)
@@ -63,7 +64,7 @@ export function VideoEmbedInnerWeb({
return (
<View
style={[a.flex_1, a.rounded_md, a.overflow_hidden]}
accessibilityLabel={_(msg`Embedded video player`)}
accessibilityLabel={l`Embedded video player`}
accessibilityHint="">
<div ref={containerRef} style={{height: '100%', width: '100%'}}>
<figure style={{margin: 0, position: 'absolute', inset: 0}}>
@@ -76,7 +77,16 @@ export function VideoEmbedInnerWeb({
muted={embed.presentation === 'gif' || !focused}
aria-labelledby={embed.alt ? figId : undefined}
onTimeUpdate={e => {
lastKnownTime.current = e.currentTarget.currentTime
const currentTime = e.currentTarget.currentTime
lastKnownTime.current = currentTime
if (
!isGif &&
reportDialogMetadata &&
Number.isFinite(currentTime) &&
currentTime >= 0
) {
reportDialogMetadata.current.videoTimestampSeconds = currentTime
}
}}
loop={loop}
/>
@@ -141,9 +151,10 @@ type CachedPromise<T> = Promise<T> & {value: undefined | T}
const promiseForHls = import(
// @ts-ignore
'hls.js/dist/hls.min'
// oxlint-disable-next-line typescript/no-unsafe-member-access
).then(mod => mod.default) as CachedPromise<typeof HlsTypes.default>
promiseForHls.value = undefined
promiseForHls.then(Hls => {
void promiseForHls.then(Hls => {
promiseForHls.value = Hls
})
@@ -166,7 +177,7 @@ function useHLS({
useEffect(() => {
if (!Hls) {
setHlsLoading(true)
promiseForHls.then(loadedHls => {
void promiseForHls.then(loadedHls => {
setHls(() => loadedHls)
setHlsLoading(false)
})
@@ -303,7 +314,7 @@ function useHLS({
hls.on(Hls.Events.ERROR, (_event, data) => {
if (data.fatal) {
if (
data.details === 'manifestLoadError' &&
(data.details as string) === 'manifestLoadError' &&
data.response?.code === 404
) {
setError(new VideoNotFoundError())
@@ -3,11 +3,11 @@ import {View} from 'react-native'
import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react'
import {formatTime} from '#/lib/media/video/formatTime'
import {clamp} from '#/lib/numbers'
import {atoms as a, useTheme, web} from '#/alf'
import {useInteractionState} from '#/components/hooks/useInteractionState'
import {IS_WEB_FIREFOX, IS_WEB_TOUCH_DEVICE} from '#/env'
import {formatTime} from './utils'
export function Scrubber({
duration,
@@ -4,6 +4,7 @@ import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react'
import {Trans} from '@lingui/react/macro'
import {formatTime} from '#/lib/media/video/formatTime'
import {clamp} from '#/lib/numbers'
import {
useAutoplayDisabled,
@@ -31,7 +32,7 @@ import {GifPresentationControls} from '../../GifPresentationControls'
import {TimeIndicator} from '../TimeIndicator'
import {ControlButton} from './ControlButton'
import {Scrubber} from './Scrubber'
import {formatTime, useVideoElement} from './utils'
import {useVideoElement} from './utils'
import {type ControlsProps} from './VideoControls.shared'
import {VolumeControl} from './VolumeControl'
@@ -238,16 +238,3 @@ export function useVideoElement(ref: RefObject<HTMLVideoElement | null>) {
canPlay,
}
}
export function formatTime(time: number) {
if (isNaN(time)) {
return '--'
}
time = Math.round(time)
const minutes = Math.floor(time / 60)
const seconds = String(time % 60).padStart(2, '0')
return `${minutes}:${seconds}`
}
+3 -2
View File
@@ -23,6 +23,7 @@ import {useInteractionState} from '#/components/hooks/useInteractionState'
import {GalleryBleed} from '#/components/images/Gallery'
import {ContentHider} from '#/components/moderation/ContentHider'
import {PostAlerts} from '#/components/moderation/PostAlerts'
import * as ReportDialogMetadataContext from '#/components/moderation/ReportDialog/ReportDialogMetadataContext'
import {StandardSiteEmbed} from '#/components/Post/Embed/StandardSiteEmbed'
import {isStandardSiteEmbed} from '#/components/Post/Embed/StandardSiteEmbed/utils'
import {RichText} from '#/components/RichText'
@@ -311,7 +312,7 @@ export function QuoteEmbed({
} = useInteractionState()
const contents = (
<>
<ReportDialogMetadataContext.Provider key={quote.uri}>
<PostMeta
author={quote.author}
moderation={moderation}
@@ -350,7 +351,7 @@ export function QuoteEmbed({
post={quote}
/>
)}
</>
</ReportDialogMetadataContext.Provider>
)
return (